2
0
mirror of https://github.com/frappe/bench.git synced 2025-01-10 09:02:10 +00:00

Merge branch 'staging' into v5.x

This commit is contained in:
Gavin D'souza 2023-06-09 17:24:52 +05:30
commit 93eedbd876
No known key found for this signature in database
GPG Key ID: 3A7BF4D4340DE6F7
10 changed files with 107 additions and 59 deletions

View File

@ -21,12 +21,17 @@ jobs:
name: Easy Install Test name: Easy Install Test
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Perform production easy install - name: Perform production easy install
run: | run: |
python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io python3 ${GITHUB_WORKSPACE}/easy-install.py -p -n actions_test --email test@frappe.io
docker compose -p actions_test exec backend bench version --format json docker compose -p actions_test exec backend bench version --format json
docker compose -p actions_test exec backend bench --site site1.local list-apps --format json docker compose -p actions_test exec backend bench --site site1.localhost list-apps --format json
result=$(curl -sk https://127.0.0.1/api/method/ping | jq -r ."message") result=$(curl -H "Host: site1.localhost" -sk https://127.0.0.1/api/method/ping | jq -r ."message")
if [[ "$result" == "pong" ]]; then echo "New instance works fine"; else exit 1; fi if [[ "$result" == "pong" ]]; then echo "New instance works fine"; else exit 1; fi
docker compose -p actions_test down docker compose -p actions_test down
docker volume prune -f docker volume prune -f

View File

@ -198,7 +198,7 @@ class App(AppMeta):
@step(title="Archiving App {repo}", success="App {repo} Archived") @step(title="Archiving App {repo}", success="App {repo} Archived")
def remove(self, no_backup: bool = False): def remove(self, no_backup: bool = False):
active_app_path = os.path.join("apps", self.repo) active_app_path = os.path.join("apps", self.app_name)
if no_backup: if no_backup:
if not os.path.islink(active_app_path): if not os.path.islink(active_app_path):
@ -209,7 +209,7 @@ class App(AppMeta):
else: else:
archived_path = os.path.join("archived", "apps") archived_path = os.path.join("archived", "apps")
archived_name = get_available_folder_name( archived_name = get_available_folder_name(
f"{self.repo}-{date.today()}", archived_path f"{self.app_name}-{date.today()}", archived_path
) )
archived_app_path = os.path.join(archived_path, archived_name) archived_app_path = os.path.join(archived_path, archived_name)
@ -233,7 +233,7 @@ class App(AppMeta):
verbose = bench.cli.verbose or verbose verbose = bench.cli.verbose or verbose
app_name = get_app_name(self.bench.name, self.app_name) app_name = get_app_name(self.bench.name, self.app_name)
if not resolved and self.repo != "frappe" and not ignore_resolution: if not resolved and self.app_name != "frappe" and not ignore_resolution:
click.secho( click.secho(
f"Ignoring dependencies of {self.name}. To install dependencies use --resolve-deps", f"Ignoring dependencies of {self.name}. To install dependencies use --resolve-deps",
fg="yellow", fg="yellow",
@ -262,7 +262,7 @@ class App(AppMeta):
from bench.utils.app import get_required_deps, required_apps_from_hooks from bench.utils.app import get_required_deps, required_apps_from_hooks
if self.on_disk: if self.on_disk:
required_deps = os.path.join(self.mount_path, self.repo, "hooks.py") required_deps = os.path.join(self.mount_path, self.app_name, "hooks.py")
try: try:
return required_apps_from_hooks(required_deps, local=True) return required_apps_from_hooks(required_deps, local=True)
except IndexError: except IndexError:
@ -290,16 +290,16 @@ def make_resolution_plan(app: App, bench: "Bench"):
decide what apps and versions to install and in what order decide what apps and versions to install and in what order
""" """
resolution = OrderedDict() resolution = OrderedDict()
resolution[app.repo] = app resolution[app.app_name] = app
for app_name in app._get_dependencies(): for app_name in app._get_dependencies():
dep_app = App(app_name, bench=bench) dep_app = App(app_name, bench=bench)
is_valid_frappe_branch(dep_app.url, dep_app.branch) is_valid_frappe_branch(dep_app.url, dep_app.branch)
dep_app.required_by = app.name dep_app.required_by = app.name
if dep_app.repo in resolution: if dep_app.app_name in resolution:
click.secho(f"{dep_app.repo} is already resolved skipping", fg="yellow") click.secho(f"{dep_app.app_name} is already resolved skipping", fg="yellow")
continue continue
resolution[dep_app.repo] = dep_app resolution[dep_app.app_name] = dep_app
resolution.update(make_resolution_plan(dep_app, bench)) resolution.update(make_resolution_plan(dep_app, bench))
app.local_resolution = [repo_name for repo_name, _ in reversed(resolution.items())] app.local_resolution = [repo_name for repo_name, _ in reversed(resolution.items())]
return resolution return resolution
@ -315,7 +315,7 @@ def get_excluded_apps(bench_path="."):
def add_to_excluded_apps_txt(app, bench_path="."): def add_to_excluded_apps_txt(app, bench_path="."):
if app == "frappe": if app == "frappe":
raise ValueError("Frappe app cannot be excludeed from update") raise ValueError("Frappe app cannot be excluded from update")
if app not in os.listdir("apps"): if app not in os.listdir("apps"):
raise ValueError(f"The app {app} does not exist") raise ValueError(f"The app {app} does not exist")
apps = get_excluded_apps(bench_path=bench_path) apps = get_excluded_apps(bench_path=bench_path)

View File

@ -131,6 +131,7 @@ class Bench(Base, Validator):
except InvalidRemoteException: except InvalidRemoteException:
if not force: if not force:
raise raise
self.apps.sync() self.apps.sync()
# self.build() - removed because it seems unnecessary # self.build() - removed because it seems unnecessary
self.reload(_raise=False) self.reload(_raise=False)
@ -309,13 +310,13 @@ class BenchApps(MutableSequence):
def add(self, app: "App"): def add(self, app: "App"):
app.get() app.get()
app.install() app.install()
super().append(app.repo) super().append(app.app_name)
self.apps.sort() self.apps.sort()
def remove(self, app: "App", no_backup: bool = False): def remove(self, app: "App", no_backup: bool = False):
app.uninstall() app.uninstall()
app.remove(no_backup=no_backup) app.remove(no_backup=no_backup)
super().remove(app.repo) super().remove(app.app_name)
def append(self, app: "App"): def append(self, app: "App"):
return self.add(app) return self.add(app)

View File

@ -28,6 +28,8 @@ from bench.utils import (
get_cmd_from_sysargv, get_cmd_from_sysargv,
) )
from bench.utils.bench import get_env_cmd from bench.utils.bench import get_env_cmd
from importlib.util import find_spec
# these variables are used to show dynamic outputs on the terminal # these variables are used to show dynamic outputs on the terminal
dynamic_feed = False dynamic_feed = False
@ -38,6 +40,7 @@ bench.LOG_BUFFER = []
change_uid_msg = "You should not run this command as root" change_uid_msg = "You should not run this command as root"
src = os.path.dirname(__file__) src = os.path.dirname(__file__)
SKIP_MODULE_TRACEBACK = ("click",)
@contextmanager @contextmanager
@ -118,6 +121,8 @@ def cli():
_opts = [x.opts + x.secondary_opts for x in bench_command.params] _opts = [x.opts + x.secondary_opts for x in bench_command.params]
opts = {item for sublist in _opts for item in sublist} opts = {item for sublist in _opts for item in sublist}
setup_exception_handler()
# handle usages like `--use-feature='feat-x'` and `--use-feature 'feat-x'` # handle usages like `--use-feature='feat-x'` and `--use-feature 'feat-x'`
if cmd_from_sys and cmd_from_sys.split("=", 1)[0].strip() in opts: if cmd_from_sys and cmd_from_sys.split("=", 1)[0].strip() in opts:
bench_command() bench_command()
@ -240,3 +245,26 @@ def setup_clear_cache():
return f(*args, **kwargs) return f(*args, **kwargs)
os.chdir = _chdir os.chdir = _chdir
def setup_exception_handler():
from traceback import format_exception
from bench.exceptions import CommandFailedError
def handle_exception(exc_type, exc_info, tb):
if exc_type == CommandFailedError:
print("".join(generate_exc(exc_type, exc_info, tb)))
else:
sys.__excepthook__(exc_type, exc_info, tb)
def generate_exc(exc_type, exc_info, tb):
TB_SKIP = [
os.path.dirname(find_spec(module).origin) for module in SKIP_MODULE_TRACEBACK
]
for tb_line in format_exception(exc_type, exc_info, tb):
for skip_module in TB_SKIP:
if skip_module not in tb_line:
yield tb_line
sys.excepthook = handle_exception

View File

@ -182,12 +182,12 @@ programs={{ bench_name }}-frappe-web {%- if node -%} ,{{ bench_name }}-node-sock
{% if use_rq %} {% if use_rq %}
[group:{{ bench_name }}-workers] [group:{{ bench_name }}-workers]
programs={{ bench_name }}-frappe-schedule,{{ bench_name }}-frappe-default-worker,{{ bench_name }}-frappe-short-worker,{{ bench_name }}-frappe-long-worker programs={{ bench_name }}-frappe-schedule,{{ bench_name }}-frappe-default-worker,{{ bench_name }}-frappe-short-worker,{{ bench_name }}-frappe-long-worker{%- for worker_name in workers -%},{{ bench_name }}-frappe-{{ worker_name }}-worker{%- endfor %}
{% else %} {% else %}
[group:{{ bench_name }}-workers] [group:{{ bench_name }}-workers]
programs={{ bench_name }}-frappe-workerbeat,{{ bench_name }}-frappe-worker,{{ bench_name }}-frappe-longjob-worker,{{ bench_name }}-frappe-async-worker programs={{ bench_name }}-frappe-workerbeat,{{ bench_name }}-frappe-worker,{{ bench_name }}-frappe-longjob-worker,{{ bench_name }}-frappe-async-worker{%- for worker_name in workers -%},{{ bench_name }}-frappe-{{ worker_name }}-worker{%- endfor %}
{% endif %} {% endif %}

View File

@ -28,8 +28,8 @@ class TestBenchInit(TestBenchBase):
self.init_bench(bench_name, **kwargs) self.init_bench(bench_name, **kwargs)
app = App("file:///tmp/frappe") app = App("file:///tmp/frappe")
self.assertTupleEqual( self.assertTupleEqual(
(app.mount_path, app.url, app.repo, app.org), (app.mount_path, app.url, app.repo, app.app_name, app.org),
("/tmp/frappe", "file:///tmp/frappe", "frappe", "frappe"), ("/tmp/frappe", "file:///tmp/frappe", "frappe", "frappe", "frappe"),
) )
self.assert_folders(bench_name) self.assert_folders(bench_name)
self.assert_virtual_env(bench_name) self.assert_virtual_env(bench_name)

View File

@ -101,4 +101,6 @@ class TestUtils(unittest.TestCase):
def test_ssh_ports(self): def test_ssh_ports(self):
app = App("git@github.com:22:frappe/frappe") app = App("git@github.com:22:frappe/frappe")
self.assertEqual((app.use_ssh, app.org, app.repo), (True, "frappe", "frappe")) self.assertEqual(
(app.use_ssh, app.org, app.repo, app.app_name), (True, "frappe", "frappe", "frappe")
)

View File

@ -155,7 +155,7 @@ def exec_cmd(cmd, cwd=".", env=None, _raise=True):
if return_code: if return_code:
logger.warning(f"{cmd_log} executed with exit code {return_code}") logger.warning(f"{cmd_log} executed with exit code {return_code}")
if _raise: if _raise:
raise CommandFailedError from subprocess.CalledProcessError(return_code, cmd) raise CommandFailedError(cmd) from subprocess.CalledProcessError(return_code, cmd)
return return_code return return_code

View File

@ -155,7 +155,7 @@ def update_npm_packages(bench_path=".", apps=None):
else: else:
package_json[key] = value package_json[key] = value
if package_json is {}: if package_json == {}:
with open(os.path.join(os.path.dirname(__file__), "package.json")) as f: with open(os.path.join(os.path.dirname(__file__), "package.json")) as f:
package_json = json.loads(f.read()) package_json = json.loads(f.read())

View File

@ -73,13 +73,13 @@ def get_from_env(dir, file) -> Dict:
def write_to_env( def write_to_env(
wd: str, wd: str,
site: str, sites,
db_pass: str, db_pass: str,
admin_pass: str, admin_pass: str,
email: str, email: str,
erpnext_version: str = None, erpnext_version: str = None,
) -> None: ) -> None:
site_name = site or "" quoted_sites = ",".join([f"`{site}`" for site in sites]).strip(",")
example_env = get_from_env(wd, "example.env") example_env = get_from_env(wd, "example.env")
erpnext_version = erpnext_version or example_env["ERPNEXT_VERSION"] erpnext_version = erpnext_version or example_env["ERPNEXT_VERSION"]
with open(os.path.join(wd, ".env"), "w") as f: with open(os.path.join(wd, ".env"), "w") as f:
@ -93,8 +93,8 @@ def write_to_env(
"REDIS_QUEUE=redis-queue:6379\n", "REDIS_QUEUE=redis-queue:6379\n",
"REDIS_SOCKETIO=redis-socketio:6379\n", "REDIS_SOCKETIO=redis-socketio:6379\n",
f"LETSENCRYPT_EMAIL={email}\n", f"LETSENCRYPT_EMAIL={email}\n",
f"FRAPPE_SITE_NAME_HEADER={site_name}\n", f"SITE_ADMIN_PASS={admin_pass}\n",
f"SITE_ADMIN_PASS={admin_pass}", f"SITES={quoted_sites}\n",
] ]
) )
@ -114,7 +114,7 @@ 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, sitename: str, email: str, version: str = None) -> None: def setup_prod(project: str, sites, email: str, version: str = None) -> None:
if check_repo_exists(): if check_repo_exists():
compose_file_name = os.path.join(os.path.expanduser("~"), f"{project}-compose.yml") compose_file_name = os.path.join(os.path.expanduser("~"), f"{project}-compose.yml")
docker_repo_path = os.path.join(os.getcwd(), "frappe_docker") docker_repo_path = os.path.join(os.getcwd(), "frappe_docker")
@ -129,7 +129,7 @@ def setup_prod(project: str, sitename: str, email: str, version: str = None) ->
if not os.path.exists(os.path.join(docker_repo_path, ".env")): if not os.path.exists(os.path.join(docker_repo_path, ".env")):
admin_pass = generate_pass() admin_pass = generate_pass()
db_pass = generate_pass(9) db_pass = generate_pass(9)
write_to_env(docker_repo_path, sitename, db_pass, admin_pass, email, version) write_to_env(docker_repo_path, sites, db_pass, admin_pass, email, version)
cprint( cprint(
"\nA .env file is generated with basic configs. Please edit it to fit to your needs \n", "\nA .env file is generated with basic configs. Please edit it to fit to your needs \n",
level=3, level=3,
@ -193,40 +193,13 @@ def setup_prod(project: str, sitename: str, email: str, version: str = None) ->
cprint(" Docker Compose failed, please check the container logs\n", e) cprint(" Docker Compose failed, please check the container logs\n", e)
sys.exit(1) sys.exit(1)
cprint(f"\nCreating site: {sitename} \n", level=3) for sitename in sites:
create_site(sitename, project, db_pass, 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("Bench site creation failed", exc_info=True)
cprint("Bench Site creation failed\n", e)
sys.exit(1)
else: else:
install_docker() install_docker()
clone_frappe_docker_repo() clone_frappe_docker_repo()
setup_prod(project, sitename, email, version) # Recursive setup_prod(project, sites, email, version) # Recursive
def setup_dev_instance(project: str): def setup_dev_instance(project: str):
@ -294,6 +267,43 @@ def install_docker():
sys.exit(1) sys.exit(1)
def create_site(
sitename: str,
project: str,
db_pass: str,
admin_pass: str,
):
cprint(f"\nCreating site: {sitename} \n", level=3)
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)
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Install Frappe with Docker") parser = argparse.ArgumentParser(description="Install Frappe with Docker")
parser.add_argument( parser.add_argument(
@ -305,8 +315,10 @@ if __name__ == "__main__":
parser.add_argument( parser.add_argument(
"-s", "-s",
"--sitename", "--sitename",
help="The Site Name for your production site", help="Site Name(s) for your production bench",
default="site1.local", default=["site1.localhost"],
action="append",
dest="sites",
) )
parser.add_argument("-n", "--project", help="Project Name", default="frappe") parser.add_argument("-n", "--project", help="Project Name", default="frappe")
parser.add_argument( parser.add_argument(
@ -326,6 +338,6 @@ if __name__ == "__main__":
if "example.com" in args.email: if "example.com" in args.email:
cprint("Emails with example.com not acceptable", level=1) cprint("Emails with example.com not acceptable", level=1)
sys.exit(1) sys.exit(1)
setup_prod(args.project, args.sitename, args.email, args.version) setup_prod(args.project, args.sites, args.email, args.version)
else: else:
parser.print_help() parser.print_help()