diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index e9ccc03f..211081a2 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -28,7 +28,7 @@ jobs: - name: Perform production easy install run: | - python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io + python3 ${GITHUB_WORKSPACE}/easy-install.py build --deploy --tag=custom-apps:latest --project=actions_test --email=test@frappe.io --image=custom-apps --version=latest --app=erpnext docker compose -p actions_test exec backend bench version --format json docker compose -p actions_test exec backend bench --site site1.localhost list-apps --format json result=$(curl -H "Host: site1.localhost" -sk https://127.0.0.1/api/method/ping | jq -r ."message") diff --git a/README.md b/README.md index 3a6eb130..c7afc665 100755 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Download the Easy Install script and execute it: ```sh $ wget https://raw.githubusercontent.com/frappe/bench/develop/easy-install.py -$ python3 easy-install.py --prod --email your@email.tld +$ python3 easy-install.py deploy --email=user@domain.tld --sitename=subdomain.domain.tld --app=erpnext ``` This script will install docker on your system and will fetch the required containers, setup bench and a default ERPNext instance. @@ -101,20 +101,150 @@ When the setup is complete, you will be able to access the system at `http:// None: - try: - urllib.request.urlretrieve( - "https://github.com/frappe/frappe_docker/archive/refs/heads/main.zip", - "frappe_docker.zip", - ) - logging.info("Downloaded frappe_docker zip file from GitHub") - unpack_archive( - "frappe_docker.zip", "." - ) # Unzipping the frappe_docker.zip creates a folder "frappe_docker-main" - move("frappe_docker-main", "frappe_docker") - logging.info("Unzipped and Renamed frappe_docker") - os.remove("frappe_docker.zip") - logging.info("Removed the downloaded zip file") - except Exception as e: - logging.error("Download and unzip failed", exc_info=True) - cprint("\nCloning frappe_docker Failed\n\n", "[ERROR]: ", e, level=1) + try: + urllib.request.urlretrieve( + "https://github.com/frappe/frappe_docker/archive/refs/heads/main.zip", + "frappe_docker.zip", + ) + logging.info("Downloaded frappe_docker zip file from GitHub") + unpack_archive("frappe_docker.zip", ".") + # Unzipping the frappe_docker.zip creates a folder "frappe_docker-main" + move("frappe_docker-main", "frappe_docker") + logging.info("Unzipped and Renamed frappe_docker") + os.remove("frappe_docker.zip") + logging.info("Removed the downloaded zip file") + except Exception as e: + logging.error("Download and unzip failed", exc_info=True) + cprint("\nCloning frappe_docker Failed\n\n", "[ERROR]: ", e, level=1) def get_from_env(dir, file) -> Dict: - env_vars = {} - with open(os.path.join(dir, file)) as f: - for line in f: - if line.startswith("#") or not line.strip(): - continue - key, value = line.strip().split("=", 1) - env_vars[key] = value - return env_vars + env_vars = {} + with open(os.path.join(dir, file)) as f: + for line in f: + if line.startswith("#") or not line.strip(): + continue + key, value = line.strip().split("=", 1) + env_vars[key] = value + return env_vars def write_to_env( - wd: str, - sites, - db_pass: str, - admin_pass: str, - email: str, - erpnext_version: str = None, + wd: str, + sites: List[str], + db_pass: str, + admin_pass: str, + email: str, + erpnext_version: str = None, + http_port: str = None, ) -> None: - quoted_sites = ",".join([f"`{site}`" for site in sites]).strip(",") - example_env = get_from_env(wd, "example.env") - erpnext_version = erpnext_version or example_env["ERPNEXT_VERSION"] - with open(os.path.join(wd, ".env"), "w") as f: - f.writelines( - [ - f"ERPNEXT_VERSION={erpnext_version}\n", # defaults to latest version of ERPNext - f"DB_PASSWORD={db_pass}\n", - "DB_HOST=db\n", - "DB_PORT=3306\n", - "REDIS_CACHE=redis-cache:6379\n", - "REDIS_QUEUE=redis-queue:6379\n", - "REDIS_SOCKETIO=redis-socketio:6379\n", - f"LETSENCRYPT_EMAIL={email}\n", - f"SITE_ADMIN_PASS={admin_pass}\n", - f"SITES={quoted_sites}\n", - ] - ) + quoted_sites = ",".join([f"`{site}`" for site in sites]).strip(",") + example_env = get_from_env(wd, "example.env") + erpnext_version = erpnext_version or example_env["ERPNEXT_VERSION"] + env_file_lines = [ + # defaults to latest version of ERPNext + f"ERPNEXT_VERSION={erpnext_version}\n", + f"DB_PASSWORD={db_pass}\n", + "DB_HOST=db\n", + "DB_PORT=3306\n", + "REDIS_CACHE=redis-cache:6379\n", + "REDIS_QUEUE=redis-queue:6379\n", + "REDIS_SOCKETIO=redis-socketio:6379\n", + f"LETSENCRYPT_EMAIL={email}\n", + f"SITE_ADMIN_PASS={admin_pass}\n", + f"SITES={quoted_sites}\n", + "PULL_POLICY=missing\n", + ] + + if http_port: + env_file_lines.append(f"HTTP_PUBLISH_PORT={http_port}\n") + + with open(os.path.join(wd, ".env"), "w") as f: + f.writelines(env_file_lines) def generate_pass(length: int = 12) -> str: - """Generate random hash using best available randomness source.""" - import math - import secrets + """Generate random hash using best available randomness source.""" + import math + import secrets - if not length: - length = 56 + if not length: + length = 56 - return secrets.token_hex(math.ceil(length / 2))[:length] + return secrets.token_hex(math.ceil(length / 2))[:length] def check_repo_exists() -> bool: - return os.path.exists(os.path.join(os.getcwd(), "frappe_docker")) + return os.path.exists(os.path.join(os.getcwd(), "frappe_docker")) -def setup_prod(project: str, sites, email: str, version: str = None, image = None) -> None: - if len(sites) == 0: - sites = ["site1.localhost"] +def start_prod( + project: str, + sites: List[str] = [], + email: str = None, + version: str = None, + image: str = None, + is_https: bool = True, + http_port: str = None, +): + if not check_repo_exists(): + clone_frappe_docker_repo() + install_container_runtime() - if check_repo_exists(): - compose_file_name = os.path.join(os.path.expanduser("~"), f"{project}-compose.yml") - docker_repo_path = os.path.join(os.getcwd(), "frappe_docker") - cprint( - "\nPlease refer to .example.env file in the frappe_docker folder to know which keys to set\n\n", - level=3, - ) - admin_pass = "" - db_pass = "" - with open(compose_file_name, "w") as f: - # Writing to compose file - if not os.path.exists(os.path.join(docker_repo_path, ".env")): - admin_pass = generate_pass() - db_pass = generate_pass(9) - write_to_env(docker_repo_path, sites, db_pass, admin_pass, email, version) - cprint( - "\nA .env file is generated with basic configs. Please edit it to fit to your needs \n", - level=3, - ) - with open(os.path.join(os.path.expanduser("~"), "passwords.txt"), "w") as en: - en.writelines(f"ADMINISTRATOR_PASSWORD={admin_pass}\n") - en.writelines(f"MARIADB_ROOT_PASSWORD={db_pass}\n") - else: - env = get_from_env(docker_repo_path, ".env") - admin_pass = env["SITE_ADMIN_PASS"] - db_pass = env["DB_PASSWORD"] - try: - # TODO: Include flags for non-https and non-erpnext installation - subprocess.run( - [ - which("docker"), - "compose", - "--project-name", - project, - "-f", - "compose.yaml", - "-f", - "overrides/compose.mariadb.yaml", - "-f", - "overrides/compose.redis.yaml", - # "-f", "overrides/compose.noproxy.yaml", TODO: Add support for local proxying without HTTPs - "-f", - "overrides/compose.https.yaml", - "--env-file", - ".env", - "config", - ], - cwd=docker_repo_path, - stdout=f, - check=True, - ) + compose_file_name = os.path.join( + os.path.expanduser("~"), + f"{project}-compose.yml", + ) + docker_repo_path = os.path.join(os.getcwd(), "frappe_docker") + cprint( + "\nPlease refer to .example.env file in the frappe_docker folder to know which keys to set\n\n", + level=3, + ) + admin_pass = "" + db_pass = "" + with open(compose_file_name, "w") as f: + # Writing to compose file + if not os.path.exists(os.path.join(docker_repo_path, ".env")): + admin_pass = generate_pass() + db_pass = generate_pass(9) + write_to_env( + wd=docker_repo_path, + sites=sites, + db_pass=db_pass, + admin_pass=admin_pass, + email=email, + erpnext_version=version, + http_port=http_port if not is_https and http_port else None, + ) + cprint( + "\nA .env file is generated with basic configs. Please edit it to fit to your needs \n", + level=3, + ) + with open( + os.path.join(os.path.expanduser("~"), "passwords.txt"), "w" + ) as en: + en.writelines(f"ADMINISTRATOR_PASSWORD={admin_pass}\n") + en.writelines(f"MARIADB_ROOT_PASSWORD={db_pass}\n") + else: + env = get_from_env(docker_repo_path, ".env") + sites = env["SITES"].replace("`", "").split(",") if env["SITES"] else [] + db_pass = env["DB_PASSWORD"] + admin_pass = env["SITE_ADMIN_PASS"] + email = env["LETSENCRYPT_EMAIL"] + write_to_env( + wd=docker_repo_path, + sites=sites, + db_pass=db_pass, + admin_pass=admin_pass, + email=email, + erpnext_version=version, + http_port=http_port if not is_https and http_port else None, + ) - except Exception: - logging.error("Docker Compose generation failed", exc_info=True) - cprint("\nGenerating Compose File failed\n") - sys.exit(1) + try: + command = [ + "docker", + "compose", + "--project-name", + project, + "-f", + "compose.yaml", + "-f", + "overrides/compose.mariadb.yaml", + "-f", + "overrides/compose.redis.yaml", + "-f", + ( + "overrides/compose.https.yaml" + if is_https + else "overrides/compose.noproxy.yaml" + ), + "--env-file", + ".env", + "config", + ] - # Use custom image - if image: - for line in fileinput.input(compose_file_name, inplace=True): - if "image: frappe/erpnext" in line: - line = line.replace("image: frappe/erpnext", f"image: {image}") - sys.stdout.write(line) + subprocess.run( + command, + cwd=docker_repo_path, + stdout=f, + check=True, + ) - try: - # Starting with generated compose file - subprocess.run( - [ - which("docker"), - "compose", - "-p", - project, - "-f", - compose_file_name, - "up", - "-d", - ], - check=True, - ) - logging.info(f"Docker Compose file generated at ~/{project}-compose.yml") + except Exception: + logging.error("Docker Compose generation failed", exc_info=True) + cprint("\nGenerating Compose File failed\n") + sys.exit(1) - except Exception as e: - logging.error("Prod docker-compose failed", exc_info=True) - cprint(" Docker Compose failed, please check the container logs\n", e) - sys.exit(1) + # Use custom image + if image: + for line in fileinput.input(compose_file_name, inplace=True): + if "image: frappe/erpnext" in line: + line = line.replace("image: frappe/erpnext", f"image: {image}") + sys.stdout.write(line) - for sitename in sites: - create_site(sitename, project, db_pass, admin_pass) + try: + # Starting with generated compose file + command = [ + "docker", + "compose", + "-p", + project, + "-f", + compose_file_name, + "up", + "--force-recreate", + "--remove-orphans", + "-d", + ] + subprocess.run( + command, + check=True, + ) + logging.info(f"Docker Compose file generated at ~/{project}-compose.yml") - else: - install_docker() - clone_frappe_docker_repo() - setup_prod(project, sites, email, version, image) # Recursive + except Exception as e: + logging.error("Prod docker-compose failed", exc_info=True) + cprint(" Docker Compose failed, please check the container logs\n", e) + sys.exit(1) + + return db_pass, admin_pass + + +def setup_prod( + project: str, + sites: List[str], + email: str, + version: str = None, + image: str = None, + apps: List[str] = [], + is_https: bool = False, + http_port: str = None, +) -> None: + if len(sites) == 0: + sites = ["site1.localhost"] + + db_pass, admin_pass = start_prod( + project=project, + sites=sites, + email=email, + version=version, + image=image, + is_https=is_https, + http_port=http_port, + ) + + for sitename in sites: + create_site(sitename, project, db_pass, admin_pass, apps) + + cprint( + f"MariaDB root password is {db_pass}", + level=2, + ) + cprint( + f"Site administrator password is {admin_pass}", + level=2, + ) + passwords_file_path = os.path.join( + os.path.expanduser("~"), + "passwords.txt", + ) + cprint(f"Passwords are stored in {passwords_file_path}", level=3) + + +def update_prod( + project: str, + version: str = None, + image=None, + is_https: bool = False, + http_port: str = None, +) -> None: + start_prod( + project=project, + version=version, + image=image, + is_https=is_https, + http_port=http_port, + ) + migrate_site(project=project) def setup_dev_instance(project: str): - if check_repo_exists(): - try: - subprocess.run( - [ - "docker", - "compose", - "-f", - "devcontainer-example/docker-compose.yml", - "--project-name", - project, - "up", - "-d", - ], - cwd=os.path.join(os.getcwd(), "frappe_docker"), - check=True, - ) - cprint( - "Please go through the Development Documentation: https://github.com/frappe/frappe_docker/tree/main/docs/development.md to fully complete the setup.", - level=2, - ) - logging.info("Development Setup completed") - except Exception as e: - logging.error("Dev Environment setup failed", exc_info=True) - cprint("Setting Up Development Environment Failed\n", e) - else: - install_docker() - clone_frappe_docker_repo() - setup_dev_instance(project) # Recursion on goes brrrr + if not check_repo_exists(): + clone_frappe_docker_repo() + install_container_runtime() + + try: + command = [ + "docker", + "compose", + "-f", + "devcontainer-example/docker-compose.yml", + "--project-name", + project, + "up", + "-d", + ] + subprocess.run( + command, + cwd=os.path.join(os.getcwd(), "frappe_docker"), + check=True, + ) + cprint( + "Please go through the Development Documentation: https://github.com/frappe/frappe_docker/tree/main/docs/development.md to fully complete the setup.", + level=2, + ) + logging.info("Development Setup completed") + except Exception as e: + logging.error("Dev Environment setup failed", exc_info=True) + cprint("Setting Up Development Environment Failed\n", e) def install_docker(): - if which("docker") is not None: - return - cprint("Docker is not installed, Installing Docker...", level=3) - logging.info("Docker not found, installing Docker") - if platform.system() == "Darwin" or platform.system() == "Windows": - print( - f""" + cprint("Docker is not installed, Installing Docker...", level=3) + logging.info("Docker not found, installing Docker") + if platform.system() == "Darwin" or platform.system() == "Windows": + cprint( + f""" This script doesn't install Docker on {"Mac" if platform.system()=="Darwin" else "Windows"}. Please go through the Docker Installation docs for your system and run this script again""" - ) - logging.debug("Docker setup failed due to platform is not Linux") - sys.exit(1) - try: - ps = subprocess.run( - ["curl", "-fsSL", "https://get.docker.com"], - capture_output=True, - check=True, - ) - subprocess.run(["/bin/bash"], input=ps.stdout, capture_output=True) - subprocess.run( - ["sudo", "usermod", "-aG", "docker", str(os.getenv("USER"))], check=True - ) - cprint("Waiting Docker to start", level=3) - time.sleep(10) - subprocess.run(["sudo", "systemctl", "restart", "docker.service"], check=True) - except Exception as e: - logging.error("Installing Docker failed", exc_info=True) - cprint("Failed to Install Docker\n", e) - cprint("\n Try Installing Docker Manually and re-run this script again\n") - sys.exit(1) + ) + logging.debug("Docker setup failed due to platform is not Linux") + sys.exit(1) + try: + ps = subprocess.run( + ["curl", "-fsSL", "https://get.docker.com"], + capture_output=True, + check=True, + ) + subprocess.run(["/bin/bash"], input=ps.stdout, capture_output=True) + subprocess.run( + [ + "sudo", + "usermod", + "-aG", + "docker", + str(os.getenv("USER")), + ], + check=True, + ) + cprint("Waiting Docker to start", level=3) + time.sleep(10) + subprocess.run( + [ + "sudo", + "systemctl", + "restart", + "docker.service", + ], + check=True, + ) + except Exception as e: + logging.error("Installing Docker failed", exc_info=True) + cprint("Failed to Install Docker\n", e) + cprint("\n Try Installing Docker Manually and re-run this script again\n") + sys.exit(1) + + +def install_container_runtime(runtime="docker"): + if which(runtime) is not None: + cprint(runtime.title() + " is already installed", level=2) + return + if runtime == "docker": + install_docker() def create_site( - sitename: str, - project: str, - db_pass: str, - admin_pass: str, + sitename: str, + project: str, + db_pass: str, + admin_pass: str, + apps: List[str] = [], ): - cprint(f"\nCreating site: {sitename} \n", level=3) + apps = apps or [] + cprint(f"\nCreating site: {sitename} \n", level=3) + command = [ + "docker", + "compose", + "-p", + project, + "exec", + "backend", + "bench", + "new-site", + "--no-mariadb-socket", + f"--db-root-password={db_pass}", + f"--admin-password={admin_pass}", + ] - try: - subprocess.run( - [ - which("docker"), - "compose", - "-p", - project, - "exec", - "backend", - "bench", - "new-site", - sitename, - "--no-mariadb-socket", - "--db-root-password", - db_pass, - "--admin-password", - admin_pass, - "--install-app", - "erpnext", - "--set-default", - ], - check=True, - ) - logging.info("New site creation completed") - except Exception as e: - logging.error(f"Bench site creation failed for {sitename}", exc_info=True) - cprint(f"Bench Site creation failed for {sitename}\n", e) + for app in apps: + command.append("--install-app") + command.append(app) + + command.append(sitename) + + try: + subprocess.run( + command, + check=True, + ) + logging.info("New site creation completed") + except Exception as e: + logging.error(f"Bench site creation failed for {sitename}", exc_info=True) + cprint(f"Bench Site creation failed for {sitename}\n", e) + + +def migrate_site(project: str): + cprint(f"\nMigrating sites for {project}", level=3) + + exec_command( + project=project, + command=[ + "bench", + "--site", + "all", + "migrate", + ], + ) + + +def exec_command(project: str, command: List[str] = [], interactive_terminal=False): + if not command: + command = ["echo", '"Please execute a command"'] + + cprint(f"\nExecuting Command:\n{' '.join(command)}", level=3) + exec_command = [ + "docker", + "compose", + "-p", + project, + "exec", + ] + + if interactive_terminal: + exec_command.append("-it") + + exec_command.append("backend") + exec_command += command + + try: + subprocess.run( + exec_command, + check=True, + ) + logging.info("New site creation completed") + except Exception as e: + logging.error(f"Exec command failed for {project}", exc_info=True) + cprint(f"Exec command failed for {project}\n", e) + + +def add_project_option(parser: argparse.ArgumentParser): + parser.add_argument( + "-n", + "--project", + help="Project Name", + default="frappe", + ) + return parser + + +def add_setup_options(parser: argparse.ArgumentParser): + parser.add_argument( + "-a", + "--app", + dest="apps", + default=[], + help="list of app(s) to be installed", + action="append", + ) + parser.add_argument( + "-s", + "--sitename", + help="Site Name(s) for your production bench", + default=[], + action="append", + dest="sites", + ) + parser.add_argument("-e", "--email", help="Add email for the SSL.") + + return parser + + +def add_common_parser(parser: argparse.ArgumentParser): + parser = add_project_option(parser) + parser.add_argument("-i", "--image", help="Full Image Name") + parser.add_argument("-q", "--no-ssl", action="store_true", help="No https") + parser.add_argument( + "-m", "--http-port", help="Http port in case of no-ssl", default="8080" + ) + parser.add_argument( + "-v", + "--version", + help="ERPNext or image version to install, defaults to latest stable", + ) + return parser + + +def add_build_parser(subparsers: argparse.ArgumentParser): + parser = subparsers.add_parser("build", help="Build custom images") + parser = add_common_parser(parser) + parser = add_setup_options(parser) + parser.add_argument( + "-p", + "--push", + help="Push the built image to registry", + action="store_true", + ) + parser.add_argument( + "-r", + "--frappe-path", + help="Frappe Repository to use, default: https://github.com/frappe/frappe", + default="https://github.com/frappe/frappe", + ) + parser.add_argument( + "-b", + "--frappe-branch", + help="Frappe branch to use, default: version-15", + default="version-15", + ) + parser.add_argument( + "-j", + "--apps-json", + help="Path to apps json, default: frappe_docker/development/apps-example.json", + default="frappe_docker/development/apps-example.json", + ) + parser.add_argument( + "-t", + "--tag", + dest="tags", + help="Full Image Name(s), default: custom-apps:latest", + action="append", + ) + parser.add_argument( + "-c", + "--containerfile", + help="Path to Containerfile: images/layered/Containerfile", + default="images/layered/Containerfile", + ) + parser.add_argument( + "-y", + "--python-version", + help="Python Version, default: 3.11.6", + default="3.11.6", + ) + parser.add_argument( + "-d", + "--node-version", + help="NodeJS Version, default: 18.18.2", + default="18.18.2", + ) + parser.add_argument( + "-x", + "--deploy", + help="Deploy after build", + action="store_true", + ) + parser.add_argument( + "-u", + "--upgrade", + help="Upgrade after build", + action="store_true", + ) + + +def add_deploy_parser(subparsers: argparse.ArgumentParser): + parser = subparsers.add_parser("deploy", help="Deploy using compose") + parser = add_common_parser(parser) + parser = add_setup_options(parser) + + +def add_develop_parser(subparsers: argparse.ArgumentParser): + parser = subparsers.add_parser("develop", help="Development setup using compose") + parser.add_argument( + "-n", "--project", default="frappe", help="Compose project name" + ) + + +def add_upgrade_parser(subparsers: argparse.ArgumentParser): + parser = subparsers.add_parser("upgrade", help="Upgrade existing project") + parser = add_common_parser(parser) + + +def add_exec_parser(subparsers: argparse.ArgumentParser): + parser = subparsers.add_parser("exec", help="Exec into existing project") + parser = add_project_option(parser) + + +def build_image( + push: bool, + frappe_path: str, + frappe_branch: str, + containerfile_path: str, + apps_json_path: str, + tags: List[str], + python_version: str, + node_version: str, +): + if not check_repo_exists(): + clone_frappe_docker_repo() + install_container_runtime() + + if not tags: + tags = ["custom-apps:latest"] + + apps_json_base64 = None + try: + with open(apps_json_path, "rb") as file_text: + file_read = file_text.read() + apps_json_base64 = ( + base64.encodebytes(file_read).decode("utf-8").replace("\n", "") + ) + except Exception as e: + logging.error("Unable to base64 encode apps.json", exc_info=True) + cprint("\nUnable to base64 encode apps.json\n\n", "[ERROR]: ", e, level=1) + + command = [ + which("docker"), + "build", + "--progress=plain", + ] + + for tag in tags: + command.append(f"--tag={tag}") + + command += [ + f"--file={containerfile_path}", + f"--build-arg=FRAPPE_PATH={frappe_path}", + f"--build-arg=FRAPPE_BRANCH={frappe_branch}", + f"--build-arg=PYTHON_VERSION={python_version}", + f"--build-arg=NODE_VERSION={node_version}", + f"--build-arg=APPS_JSON_BASE64={apps_json_base64}", + ".", + ] + + try: + subprocess.run( + command, + check=True, + cwd="frappe_docker", + ) + except Exception as e: + logging.error("Image build failed", exc_info=True) + cprint("\nImage build failed\n\n", "[ERROR]: ", e, level=1) + + if push: + try: + for tag in tags: + subprocess.run( + [which("docker"), "push", tag], + check=True, + ) + except Exception as e: + logging.error("Image push failed", exc_info=True) + cprint("\nImage push failed\n\n", "[ERROR]: ", e, level=1) + + +def get_args_parser(): + parser = argparse.ArgumentParser( + description="Easy install script for Frappe Framework" + ) + # Setup sub-commands + subparsers = parser.add_subparsers(dest="subcommand") + # Build command + add_build_parser(subparsers) + # Deploy command + add_deploy_parser(subparsers) + # Upgrade command + add_upgrade_parser(subparsers) + # Develop command + add_develop_parser(subparsers) + # Exec command + add_exec_parser(subparsers) + + return parser if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Install Frappe with Docker") - parser.add_argument( - "-p", "--prod", help="Setup Production System", action="store_true" - ) - parser.add_argument( - "-d", "--dev", help="Setup Development System", action="store_true" - ) - parser.add_argument( - "-s", - "--sitename", - help="Site Name(s) for your production bench", - default=[], - action="append", - dest="sites", - ) - parser.add_argument("-n", "--project", help="Project Name", default="frappe") - parser.add_argument("-i", "--image", help="Full Image Name") - parser.add_argument( - "--email", help="Add email for the SSL.", required="--prod" in sys.argv - ) - parser.add_argument( - "-v", "--version", help="ERPNext version to install, defaults to latest stable" - ) - args = parser.parse_args() - if args.dev: - cprint("\nSetting Up Development Instance\n", level=2) - logging.info("Running Development Setup") - setup_dev_instance(args.project) - elif args.prod: - cprint("\nSetting Up Production Instance\n", level=2) - logging.info("Running Production Setup") - if "example.com" in args.email: - cprint("Emails with example.com not acceptable", level=1) - sys.exit(1) - setup_prod(args.project, args.sites, args.email, args.version, args.image) - else: - parser.print_help() + parser = get_args_parser() + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) + + args = parser.parse_args() + + if args.subcommand == "build": + build_image( + push=args.push, + frappe_path=args.frappe_path, + frappe_branch=args.frappe_branch, + apps_json_path=args.apps_json, + tags=args.tags, + containerfile_path=args.containerfile, + python_version=args.python_version, + node_version=args.node_version, + ) + if args.deploy: + setup_prod( + project=args.project, + sites=args.sites, + email=args.email, + version=args.version, + image=args.image, + apps=args.apps, + is_https=not args.no_ssl, + http_port=args.http_port, + ) + elif args.upgrade: + update_prod( + project=args.project, + version=args.version, + image=args.image, + is_https=not args.no_ssl, + http_port=args.http_port, + ) + + elif args.subcommand == "deploy": + cprint("\nSetting Up Production Instance\n", level=2) + logging.info("Running Production Setup") + if "example.com" in args.email: + cprint("Emails with example.com not acceptable", level=1) + sys.exit(1) + setup_prod( + project=args.project, + sites=args.sites, + email=args.email, + version=args.version, + image=args.image, + apps=args.apps, + is_https=not args.no_ssl, + http_port=args.http_port, + ) + elif args.subcommand == "develop": + cprint("\nSetting Up Development Instance\n", level=2) + logging.info("Running Development Setup") + setup_dev_instance(args.project) + elif args.subcommand == "upgrade": + cprint("\nUpgrading Production Instance\n", level=2) + logging.info("Upgrading Development Setup") + update_prod( + project=args.project, + version=args.version, + image=args.image, + is_https=not args.no_ssl, + http_port=args.http_port, + ) + elif args.subcommand == "exec": + cprint(f"\nExec into {args.project} backend\n", level=2) + logging.info(f"Exec into {args.project} backend") + exec_command( + project=args.project, + command=["bash"], + interactive_terminal=True, + )