From 1f3f68cbb6994187c07c5de410002a378163975e Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 14:56:02 +0530 Subject: [PATCH 01/14] feat: easy-install.py to build custom images --- .github/workflows/easy-install.yml | 3 +- easy-install.py | 130 +++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index e9ccc03f..cb909eab 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -28,7 +28,8 @@ 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 + python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image custom-apps:latest 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/easy-install.py b/easy-install.py index 2e22e219..9bb01f92 100755 --- a/easy-install.py +++ b/easy-install.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +import base64 import fileinput import logging import os @@ -316,8 +317,123 @@ def create_site( cprint(f"Bench Site creation failed for {sitename}\n", e) +def add_build_parser(argparser: argparse.ArgumentParser): + subparsers = argparser.add_subparsers(dest='subcommand') + build = subparsers.add_parser('build', help='Build Custom Images') + build.add_argument( + "-p", + "--push", + help="Push the built image to registry", + action="store_true", + ) + build.add_argument( + "-r", + "--frappe-path", + help="Frappe Repository to use, default: https://github.com/frappe/frappe", # noqa: E501 + default="https://github.com/frappe/frappe", + ) + build.add_argument( + "-b", + "--frappe-branch", + help="Frappe branch to use, default: version-15", + default="version-15", + ) + build.add_argument( + "-j", + "--apps-json", + help="Path to apps json, default: frappe_docker/development/apps-example.json", + default="frappe_docker/development/apps-example.json", + ) + build.add_argument( + "-i", + "--image-name", + help="Full Image Name, default: custom-apps:latest", + default="custom-apps:latest", + ) + build.add_argument( + "-c", + "--containerfile", + help="Path to Containerfile: images/layered/Containerfile", + default="images/layered/Containerfile", + ) + build.add_argument( + "-y", + "--python-version", + help="Python Version, default: 3.11.6", + default="3.11.6", + ) + build.add_argument( + "-n", + "--node-version", + help="NodeJS Version, default: 18.18.2", + default="18.18.2", + ) + +def build_image( + push: bool, + frappe_path: str, + frappe_branch: str, + containerfile_path: str, + apps_json_path: str, + image_name: str, + python_version: str, + node_version: str, +): + if not check_repo_exists(): + clone_frappe_docker_repo() + install_docker() + 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", + f"--tag={image_name}", + 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: + subprocess.run( + [which("docker"), "push", image_name], + check=True, + ) + except Exception as e: + logging.error("Image push failed", exc_info=True) + cprint("\nImage push failed\n\n", "[ERROR]: ", e, level=1) + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Install Frappe with Docker") + + # Build command + add_build_parser(parser) + parser.add_argument( "-p", "--prod", help="Setup Production System", action="store_true" ) @@ -341,6 +457,20 @@ if __name__ == "__main__": "-v", "--version", help="ERPNext version to install, defaults to latest stable" ) 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, + image_name=args.image_name, + containerfile_path=args.containerfile, + python_version=args.python_version, + node_version=args.node_version, + ) + sys.exit(0) + if args.dev: cprint("\nSetting Up Development Instance\n", level=2) logging.info("Running Development Setup") From 18426dbdf19af04ebd4fe0dbbc3af8552483d886 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 15:02:06 +0530 Subject: [PATCH 02/14] fix: easy-install.py build command arg for image --- .github/workflows/easy-install.yml | 4 ++-- easy-install.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index cb909eab..e7ef8f3a 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -28,8 +28,8 @@ jobs: - name: Perform production easy install run: | - python3 ${GITHUB_WORKSPACE}/easy-install.py build - python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image custom-apps:latest + python3 ${GITHUB_WORKSPACE}/easy-install.py build --image erpnext:version-15 + python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image erpnext:version-15 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/easy-install.py b/easy-install.py index 9bb01f92..b0a99d06 100755 --- a/easy-install.py +++ b/easy-install.py @@ -346,7 +346,7 @@ def add_build_parser(argparser: argparse.ArgumentParser): ) build.add_argument( "-i", - "--image-name", + "--image", help="Full Image Name, default: custom-apps:latest", default="custom-apps:latest", ) @@ -464,7 +464,7 @@ if __name__ == "__main__": frappe_path=args.frappe_path, frappe_branch=args.frappe_branch, apps_json_path=args.apps_json, - image_name=args.image_name, + image_name=args.image, containerfile_path=args.containerfile, python_version=args.python_version, node_version=args.node_version, From 97ce6f79a9d8ffa18d7c2c4d6fa62a621aed6a47 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 15:20:01 +0530 Subject: [PATCH 03/14] ci: fix easy-install args in test --- .github/workflows/easy-install.yml | 4 ++-- easy-install.py | 30 +++++++++++++++++++----------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index e7ef8f3a..2de7882d 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -28,8 +28,8 @@ jobs: - name: Perform production easy install run: | - python3 ${GITHUB_WORKSPACE}/easy-install.py build --image erpnext:version-15 - python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image erpnext:version-15 + python3 ${GITHUB_WORKSPACE}/easy-install.py build + python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image custom-apps --version latest 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/easy-install.py b/easy-install.py index b0a99d06..5e0e5fc6 100755 --- a/easy-install.py +++ b/easy-install.py @@ -345,10 +345,12 @@ def add_build_parser(argparser: argparse.ArgumentParser): default="frappe_docker/development/apps-example.json", ) build.add_argument( - "-i", - "--image", - help="Full Image Name, default: custom-apps:latest", - default="custom-apps:latest", + "-t", + "--tag", + dest="tags", + help="Full Image Name(s), default: custom-apps:latest", + action="append", + default=["custom-apps:latest"], ) build.add_argument( "-c", @@ -375,7 +377,7 @@ def build_image( frappe_branch: str, containerfile_path: str, apps_json_path: str, - image_name: str, + tags: list[str], python_version: str, node_version: str, ): @@ -397,7 +399,12 @@ def build_image( which("docker"), "build", "--progress=plain", - f"--tag={image_name}", + ] + + 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}", @@ -419,10 +426,11 @@ def build_image( if push: try: - subprocess.run( - [which("docker"), "push", image_name], - check=True, - ) + 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) @@ -464,7 +472,7 @@ if __name__ == "__main__": frappe_path=args.frappe_path, frappe_branch=args.frappe_branch, apps_json_path=args.apps_json, - image_name=args.image, + tags=args.tags, containerfile_path=args.containerfile, python_version=args.python_version, node_version=args.node_version, From 6f407fe317c770d4b04d11acd0f4af08dc204278 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 15:24:40 +0530 Subject: [PATCH 04/14] fix: easy-install.py typings --- easy-install.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easy-install.py b/easy-install.py index 5e0e5fc6..e297144b 100755 --- a/easy-install.py +++ b/easy-install.py @@ -11,7 +11,7 @@ import sys import time import urllib.request from shutil import move, unpack_archive, which -from typing import Dict +from typing import Dict, List logging.basicConfig( filename="easy-install.log", @@ -377,7 +377,7 @@ def build_image( frappe_branch: str, containerfile_path: str, apps_json_path: str, - tags: list[str], + tags: List[str], python_version: str, node_version: str, ): From 4e7e7c07670e6bf63c5b55c1409a9502170c9e96 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 15:37:42 +0530 Subject: [PATCH 05/14] ci: use local image registry for easy-install test --- .github/workflows/easy-install.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index 2de7882d..49f34586 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -28,8 +28,8 @@ jobs: - name: Perform production easy install run: | - python3 ${GITHUB_WORKSPACE}/easy-install.py build - python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image custom-apps --version latest + python3 ${GITHUB_WORKSPACE}/easy-install.py build --tag localhost:5000/frappe/erpnext:latest --push + python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image localhost:5000/frappe/erpnext --version latest 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") From 1e32f3831646a196c9203ff7d703966564bbc454 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 15:41:23 +0530 Subject: [PATCH 06/14] ci: setup local image registry for easy-install test --- .github/workflows/easy-install.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index 49f34586..f7184a69 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -17,6 +17,11 @@ jobs: easy-install-setup: runs-on: ubuntu-latest timeout-minutes: 60 + services: + registry: + image: registry:2 + ports: + - 5000:5000 name: Easy Install Test steps: From 82e4b2c148df0cb9fb9fdae8cd26d0f763a185f4 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 15:50:28 +0530 Subject: [PATCH 07/14] ci: login to local image registry for easy-install test --- .github/workflows/easy-install.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index f7184a69..5a216d5b 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -31,6 +31,13 @@ jobs: with: python-version: '3.8' + - name: Login + uses: docker/login-action@v3 + with: + registry: localhost:5000 + username: admin + password: secret + - name: Perform production easy install run: | python3 ${GITHUB_WORKSPACE}/easy-install.py build --tag localhost:5000/frappe/erpnext:latest --push From 6c97d929172665f3c4ca1e9c32740f58ae5ab490 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 15:58:16 +0530 Subject: [PATCH 08/14] fix: easy-install.py build to set tag only if empty --- easy-install.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/easy-install.py b/easy-install.py index e297144b..2b3c24ff 100755 --- a/easy-install.py +++ b/easy-install.py @@ -350,7 +350,6 @@ def add_build_parser(argparser: argparse.ArgumentParser): dest="tags", help="Full Image Name(s), default: custom-apps:latest", action="append", - default=["custom-apps:latest"], ) build.add_argument( "-c", @@ -384,6 +383,10 @@ def build_image( if not check_repo_exists(): clone_frappe_docker_repo() install_docker() + + if not tags: + tags = ["custom-apps:latest"] + apps_json_base64 = None try: with open(apps_json_path, "rb") as file_text: From 502c9a2ccc4fe7f8f283aa0b7b096735b44f5388 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 27 Oct 2024 16:13:32 +0530 Subject: [PATCH 09/14] ci: remove registry use local built image --- .github/workflows/easy-install.yml | 16 ++-------------- easy-install.py | 1 + 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index 5a216d5b..2de7882d 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -17,11 +17,6 @@ jobs: easy-install-setup: runs-on: ubuntu-latest timeout-minutes: 60 - services: - registry: - image: registry:2 - ports: - - 5000:5000 name: Easy Install Test steps: @@ -31,17 +26,10 @@ jobs: with: python-version: '3.8' - - name: Login - uses: docker/login-action@v3 - with: - registry: localhost:5000 - username: admin - password: secret - - name: Perform production easy install run: | - python3 ${GITHUB_WORKSPACE}/easy-install.py build --tag localhost:5000/frappe/erpnext:latest --push - python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image localhost:5000/frappe/erpnext --version latest + python3 ${GITHUB_WORKSPACE}/easy-install.py build + python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image custom-apps --version latest 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/easy-install.py b/easy-install.py index 2b3c24ff..99aafd4d 100755 --- a/easy-install.py +++ b/easy-install.py @@ -97,6 +97,7 @@ def write_to_env( f"LETSENCRYPT_EMAIL={email}\n", f"SITE_ADMIN_PASS={admin_pass}\n", f"SITES={quoted_sites}\n", + "PULL_POLICY=missing\n", ] ) From c07dfc59f3a38e2560ced7e767e456931b5a2e1e Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Tue, 29 Oct 2024 12:09:48 +0530 Subject: [PATCH 10/14] fix(easy-install): install app configurable --- .github/workflows/easy-install.yml | 2 +- easy-install.py | 59 ++++++++++++++++++------------ 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index 2de7882d..d602ac97 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -29,7 +29,7 @@ jobs: - name: Perform production easy install run: | python3 ${GITHUB_WORKSPACE}/easy-install.py build - python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image custom-apps --version latest + python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n 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/easy-install.py b/easy-install.py index 99aafd4d..783d6cce 100755 --- a/easy-install.py +++ b/easy-install.py @@ -117,7 +117,7 @@ def check_repo_exists() -> bool: 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: +def setup_prod(project: str, sites, email: str, version: str = None, image = None, apps = []) -> None: if len(sites) == 0: sites = ["site1.localhost"] @@ -208,12 +208,12 @@ def setup_prod(project: str, sites, email: str, version: str = None, image = Non sys.exit(1) for sitename in sites: - create_site(sitename, project, db_pass, admin_pass) + create_site(sitename, project, db_pass, admin_pass, apps) else: install_docker() clone_frappe_docker_repo() - setup_prod(project, sites, email, version, image) # Recursive + setup_prod(project, sites, email, version, image, apps) # Recursive def setup_dev_instance(project: str): @@ -286,30 +286,34 @@ def create_site( project: str, db_pass: str, admin_pass: str, + apps: List[str] = [], ): cprint(f"\nCreating site: {sitename} \n", level=3) + command = [ + which("docker"), + "compose", + "-p", + project, + "exec", + "backend", + "bench", + "new-site", + sitename, + "--no-mariadb-socket", + "--db-root-password", + db_pass, + "--admin-password", + admin_pass, + "--set-default", + ] + + for app in apps: + command.append("--install-app") + command.append(app) 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", - ], + command, check=True, ) logging.info("New site creation completed") @@ -330,7 +334,7 @@ def add_build_parser(argparser: argparse.ArgumentParser): build.add_argument( "-r", "--frappe-path", - help="Frappe Repository to use, default: https://github.com/frappe/frappe", # noqa: E501 + help="Frappe Repository to use, default: https://github.com/frappe/frappe", default="https://github.com/frappe/frappe", ) build.add_argument( @@ -468,6 +472,13 @@ if __name__ == "__main__": parser.add_argument( "-v", "--version", help="ERPNext version to install, defaults to latest stable" ) + parser.add_argument( + "-a", + "--app", + dest="apps", + help="list of app(s) to be installed", + action="append", + ) args = parser.parse_args() if args.subcommand == 'build': @@ -493,6 +504,6 @@ if __name__ == "__main__": 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) + setup_prod(args.project, args.sites, args.email, args.version, args.image, args.apps) else: parser.print_help() From 71d05d9d5691407b15e691eb85a661ca1d84ed59 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Tue, 29 Oct 2024 12:17:12 +0530 Subject: [PATCH 11/14] ci: do not output build unless error --- .github/workflows/easy-install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index d602ac97..4253d89b 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 build + python3 ${GITHUB_WORKSPACE}/easy-install.py build 2>&1 >/dev/null python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n 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 From 7931b393347c2fe9c5ee8b5560383f7662dc7c2a Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Tue, 29 Oct 2024 12:22:57 +0530 Subject: [PATCH 12/14] ci(easy-install): supress build output --- .github/workflows/easy-install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index 4253d89b..d30fdbd6 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 build 2>&1 >/dev/null + python3 ${GITHUB_WORKSPACE}/easy-install.py build >/dev/null python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n 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 From df24b714cc9d9df4bee2ecd7eef8d02dd1d15f7a Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Thu, 7 Nov 2024 16:15:58 +0530 Subject: [PATCH 13/14] feat: sub commands for easy-install --- .github/workflows/easy-install.yml | 3 +- README.md | 150 +++- easy-install.py | 1134 +++++++++++++++++----------- 3 files changed, 847 insertions(+), 440 deletions(-) diff --git a/.github/workflows/easy-install.yml b/.github/workflows/easy-install.yml index d30fdbd6..211081a2 100644 --- a/.github/workflows/easy-install.yml +++ b/.github/workflows/easy-install.yml @@ -28,8 +28,7 @@ jobs: - name: Perform production easy install run: | - python3 ${GITHUB_WORKSPACE}/easy-install.py build >/dev/null - python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io --image custom-apps --version latest --app erpnext + 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", - "PULL_POLICY=missing\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, apps = []) -> 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, apps) + 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, apps) # 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, - apps: List[str] = [], + sitename: str, + project: str, + db_pass: str, + admin_pass: str, + apps: List[str] = [], ): - cprint(f"\nCreating site: {sitename} \n", level=3) - command = [ - which("docker"), - "compose", - "-p", - project, - "exec", - "backend", - "bench", - "new-site", - sitename, - "--no-mariadb-socket", - "--db-root-password", - db_pass, - "--admin-password", - admin_pass, - "--set-default", - ] + 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}", + ] - for app in apps: - command.append("--install-app") - command.append(app) + for app in apps: + command.append("--install-app") + command.append(app) - 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) + 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 add_build_parser(argparser: argparse.ArgumentParser): - subparsers = argparser.add_subparsers(dest='subcommand') - build = subparsers.add_parser('build', help='Build Custom Images') - build.add_argument( - "-p", - "--push", - help="Push the built image to registry", - action="store_true", - ) - build.add_argument( - "-r", - "--frappe-path", - help="Frappe Repository to use, default: https://github.com/frappe/frappe", - default="https://github.com/frappe/frappe", - ) - build.add_argument( - "-b", - "--frappe-branch", - help="Frappe branch to use, default: version-15", - default="version-15", - ) - build.add_argument( - "-j", - "--apps-json", - help="Path to apps json, default: frappe_docker/development/apps-example.json", - default="frappe_docker/development/apps-example.json", - ) - build.add_argument( - "-t", - "--tag", - dest="tags", - help="Full Image Name(s), default: custom-apps:latest", - action="append", - ) - build.add_argument( - "-c", - "--containerfile", - help="Path to Containerfile: images/layered/Containerfile", - default="images/layered/Containerfile", - ) - build.add_argument( - "-y", - "--python-version", - help="Python Version, default: 3.11.6", - default="3.11.6", - ) - build.add_argument( - "-n", - "--node-version", - help="NodeJS Version, default: 18.18.2", - default="18.18.2", - ) +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, + 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_docker() + if not check_repo_exists(): + clone_frappe_docker_repo() + install_container_runtime() - if not tags: - tags = ["custom-apps:latest"] + 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) + 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", - ] + command = [ + which("docker"), + "build", + "--progress=plain", + ] - for tag in tags: - command.append(f"--tag={tag}") + 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}", - ".", - ] + 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) + 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) + 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 = get_args_parser() + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) - # Build command - add_build_parser(parser) + args = parser.parse_args() - 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" - ) - parser.add_argument( - "-a", - "--app", - dest="apps", - help="list of app(s) to be installed", - action="append", - ) - 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, + ) - 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, - ) - sys.exit(0) - - 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, args.apps) - else: - parser.print_help() + 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("\nSetting Up Development Instance\n", level=2) + logging.info("Running 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, + ) From da5ba67e1761a0bc369ca7bec095b5ed5a6834d0 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 11 Nov 2024 14:04:15 +0530 Subject: [PATCH 14/14] fix(easy-install.py): fix message for upgrading setup --- easy-install.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easy-install.py b/easy-install.py index 8dccf637..3a61b277 100755 --- a/easy-install.py +++ b/easy-install.py @@ -768,8 +768,8 @@ if __name__ == "__main__": logging.info("Running Development Setup") setup_dev_instance(args.project) elif args.subcommand == "upgrade": - cprint("\nSetting Up Development Instance\n", level=2) - logging.info("Running Development Setup") + cprint("\nUpgrading Production Instance\n", level=2) + logging.info("Upgrading Development Setup") update_prod( project=args.project, version=args.version,