# Copyright (c) 2019, Frappe and contributors # For license information, please see license.txt from __future__ import annotations import re from collections import OrderedDict from typing import TYPE_CHECKING import jingrow from frappe.core.utils import find, find_all from frappe.model.naming import append_number_if_name_exists from frappe.utils import flt, sbool from press.api.github import branches from press.api.site import protected from press.press.doctype.agent_job.agent_job import job_detail from press.press.doctype.app_patch.app_patch import create_app_patch from press.press.doctype.bench_update.bench_update import get_bench_update from press.press.doctype.cluster.cluster import Cluster from press.press.doctype.deploy_candidate_build.deploy_candidate_build import ( fail_and_redeploy as fail_and_redeploy_build, ) from press.press.doctype.deploy_candidate_build.deploy_candidate_build import fail_remote_job from press.press.doctype.deploy_candidate_build.deploy_candidate_build import redeploy as redeploy_candidate from press.press.doctype.marketplace_app.marketplace_app import ( get_total_installs_by_app, ) from press.press.doctype.release_group.release_group import ( ReleaseGroup, new_release_group, ) from press.press.doctype.team.team import get_child_team_members from press.utils import ( get_app_tag, get_client_blacklisted_keys, get_current_team, unique, ) if TYPE_CHECKING: from press.press.doctype.app_source.app_source import AppSource from press.press.doctype.bench.bench import Bench from press.press.doctype.deploy_candidate.deploy_candidate import DeployCandidate from press.press.doctype.deploy_candidate_build.deploy_candidate_build import DeployCandidateBuild @jingrow.whitelist() def new(bench): team = get_current_team(get_pg=True) if not team.enabled: frappe.throw("You cannot create a new bench because your account is disabled") if exists(bench["title"]): frappe.throw("A bench exists with the same name") if bench["server"] and not ( frappe.session.data.user_type == "System User" or frappe.db.get_value("Server", bench["server"], "team") == team.name ): frappe.throw("You can only create benches on your servers") apps = [{"app": app["name"], "source": app["source"]} for app in bench["apps"]] group = new_release_group( bench["title"], bench["version"], apps, team.name, bench["cluster"], bench["saas_app"] if frappe.db.exists("Saas App", bench["saas_app"]) else "", bench["server"], ) return group.name @jingrow.whitelist() @protected("Release Group") def get(name): group = frappe.get_pg("Release Group", name) return { "name": group.name, "title": group.title, "team": group.team, "version": group.version, "status": get_group_status(name), "last_updated": group.modified, "creation": group.creation, "saas_app": group.saas_app or "", "public": group.public, "no_sites": frappe.db.count("Site", {"group": group.name, "status": "Active"}), "bench_tags": [{"name": x.tag, "tag": x.tag_name} for x in group.tags], "tags": frappe.get_all( "Press Tag", {"team": group.team, "doctype_name": "Release Group"}, ["name", "tag"] ), } def get_group_status(name): active_benches = frappe.get_all( "Bench", {"group": name, "status": "Active"}, limit=1, order_by="creation desc" ) return "Active" if active_benches else "Awaiting Deploy" @jingrow.whitelist() def all(server=None, bench_filter=None): if bench_filter is None: bench_filter = {"status": "", "tag": ""} team = get_current_team() child_teams = [team.name for team in get_child_team_members(team)] teams = [team, *child_teams] group = frappe.qb.DocType("Release Group") site = frappe.qb.DocType("Site") query = ( frappe.qb.from_(group) .left_join(site) .on((site.group == group.name) & (site.status != "Archived")) .where((group.enabled == 1) & (group.public == 0)) .where((group.team).isin(teams)) .groupby(group.name) .select( frappe.query_builder.functions.Count(site.name).as_("number_of_sites"), group.name, group.title, group.version, group.creation, ) .orderby(group.title, order=frappe.qb.desc) ) bench = frappe.qb.DocType("Bench") if bench_filter["status"] == "Active": query = query.inner_join(bench).on(group.name == bench.group) elif bench_filter["status"] == "Awaiting Deploy": group_names = frappe.get_all("Bench", {"status": "Active"}, pluck="group", distinct=True) query = query.inner_join(bench).on(group.name.notin(group_names)) if bench_filter["tag"]: press_tag = frappe.qb.DocType("Resource Tag") query = query.inner_join(press_tag).on( (press_tag.tag_name == bench_filter["tag"]) & (press_tag.parent == group.name) ) if server: group_server = frappe.qb.DocType("Release Group Server") query = ( query.inner_join(group_server) .on(group_server.parent == group.name) .where(group_server.server == server) ) private_groups = query.run(as_dict=True) if not private_groups: return [] app_counts = get_app_counts_for_groups([rg.name for rg in private_groups]) for group in private_groups: group.tags = frappe.get_all("Resource Tag", {"parent": group.name}, pluck="tag_name") group.number_of_apps = app_counts[group.name] group.status = get_group_status(group.name) return private_groups @jingrow.whitelist() def bench_tags(): team = get_current_team() return frappe.get_all("Press Tag", {"team": team, "doctype_name": "Release Group"}, pluck="tag") def get_app_counts_for_groups(rg_names): rg_app = frappe.qb.DocType("Release Group App") app_counts = ( frappe.qb.from_(rg_app) .where(rg_app.parent.isin(rg_names)) .groupby(rg_app.parent) .select( rg_app.parent, frappe.query_builder.functions.Count("*"), ) .run() ) app_counts_map = {} for rg_name, app_count in app_counts: app_counts_map[rg_name] = app_count return app_counts_map @jingrow.whitelist() def exists(title): team = get_current_team() return bool(frappe.db.exists("Release Group", {"title": title, "team": team, "enabled": True})) @jingrow.whitelist() def get_default_apps(): press_settings = frappe.get_single("Press Settings") default_apps = press_settings.get_default_apps() versions, rows = get_app_versions_list() version_based_default_apps = {v.version: [] for v in versions} for app in default_apps: for row in filter(lambda x: x.app == app, rows): version_based_default_apps[row.version].append(row) return version_based_default_apps def get_app_versions_list(only_frappe=False): AppSource = frappe.qb.DocType("App Source") FrappeVersion = frappe.qb.DocType("Frappe Version") AppSourceVersion = frappe.qb.DocType("App Source Version") rows = ( frappe.qb.from_(AppSourceVersion) .left_join(AppSource) .on(AppSourceVersion.parent == AppSource.name) .left_join(FrappeVersion) .on(AppSourceVersion.version == FrappeVersion.name) .where((AppSource.enabled == 1) & (AppSource.public == 1) & (FrappeVersion.public == 1)) .select( FrappeVersion.name.as_("version"), FrappeVersion.status, FrappeVersion.default, AppSource.name.as_("source"), AppSource.app, AppSource.repository_url, AppSource.repository, AppSource.repository_owner, AppSource.branch, AppSource.app_title.as_("title"), AppSource.frappe, ) .orderby(AppSource.creation) ) if only_frappe: rows = rows.where(AppSource.frappe == 1) rows = rows.run(as_dict=True) version_list = unique(rows, lambda x: x.version) return version_list, rows @jingrow.whitelist() def options(): version_list, rows = get_app_versions_list(only_frappe=True) approved_apps = frappe.get_all("Marketplace App", filters={"frappe_approved": 1}, pluck="app") versions = [] for d in version_list: version_dict = {"name": d.version, "status": d.status, "default": d.default} version_rows = find_all(rows, lambda x: x.version == d.version) app_list = frappe.utils.unique([row.app for row in version_rows]) app_list = sorted(app_list, key=lambda x: x not in approved_apps) for app in app_list: app_rows = find_all(version_rows, lambda x: x.app == app) app_dict = {"name": app, "title": app_rows[0].title} for source in app_rows: source_dict = { "name": source.source, "repository_url": source.repository_url, "branch": source.branch, "repository": source.repository, "repository_owner": source.repository_owner, } app_dict.setdefault("sources", []).append(source_dict) app_dict["source"] = app_dict["sources"][0] version_dict.setdefault("apps", []).append(app_dict) versions.append(version_dict) clusters = Cluster.get_all_for_new_bench() if not versions: frappe.throw("Only enabled and public app sources will reflect here!") return {"versions": versions, "clusters": clusters} @jingrow.whitelist() @protected("Release Group") def bench_config(name): rg = frappe.get_pg("Release Group", name) common_site_config = [ {"key": config.key, "value": config.value, "type": config.type} for config in rg.common_site_config_table if not config.internal ] bench_config = frappe.parse_json(rg.bench_config) if bench_config.get("http_timeout"): bench_config = [ frappe._dict( key="http_timeout", value=bench_config.get("http_timeout"), type="Number", internal=False, ) ] else: bench_config = [] config = common_site_config + bench_config secret_keys = frappe.get_all("Site Config Key", filters={"type": "Password"}, pluck="key") for c in config: if c["key"] in secret_keys: c["value"] = "*******" c["type"] = "Password" return config @jingrow.whitelist() @protected("Release Group") def update_config(name, config): sanitized_common_site_config, sanitized_bench_config = [], [] bench_config_keys = ["http_timeout"] config = frappe.parse_json(config) config = [frappe._dict(c) for c in config] for c in config: if c.key in get_client_blacklisted_keys(): continue if frappe.db.exists("Site Config Key", c.key): c.type = frappe.db.get_value("Site Config Key", c.key, "type") format_config_value(name, c) if c.key in bench_config_keys: sanitized_bench_config.append(c) else: sanitized_common_site_config.append(c) rg: ReleaseGroup = frappe.get_pg("Release Group", name) rg.update_config_in_release_group(sanitized_common_site_config, sanitized_bench_config) rg.update_benches_config() return list(filter(lambda x: not x.internal, rg.common_site_config_table)) def format_config_value(group: str, c: frappe._dict): if c.type == "Number": c.value = flt(c.value) elif c.type == "Boolean": c.value = bool(sbool(c.value)) elif c.type == "JSON": c.value = frappe.parse_json(c.value) elif c.type == "Password" and c.value == "*******": c.value = frappe.get_value("Site Config", {"key": c.key, "parent": group}, "value") @jingrow.whitelist() @protected("Release Group") def dependencies(name: str): rg: ReleaseGroup = frappe.get_pg("Release Group", name) active_dependencies = [{"key": d.dependency, "value": d.version} for d in rg.dependencies] supported_dependencies = frappe.db.get_all( "Bench Dependency Version", {"supported_frappe_version": rg.version}, ["parent as `key`", "version as `value`"], ) bench_dependencies = frappe.get_all("Bench Dependency", ["name", "title", "internal"]) return { "active_dependencies": active_dependencies, "supported_dependencies": list( # deduplicate dependencies {d["value"]: d for d in supported_dependencies + active_dependencies}.values() ), "dependency_title": {d["name"]: d["title"] for d in bench_dependencies}, "internal_dependencies": [d["name"] for d in bench_dependencies if d["internal"]], "update_available": rg.dependency_update_pending, } @jingrow.whitelist() @protected("Release Group") def update_dependencies(name: str, dependencies: str): dependencies = frappe.parse_json(dependencies) rg: ReleaseGroup = frappe.get_pg("Release Group", name) if len(rg.dependencies) != len(dependencies): frappe.throw("Need all required dependencies") if diff := set([d["key"] for d in dependencies]) - set(d.dependency for d in rg.dependencies): frappe.throw("Invalid dependencies: " + ", ".join(diff)) for dep, new in zip( sorted(rg.dependencies, key=lambda x: x.dependency), sorted(dependencies, key=lambda x: x["key"]), strict=False, ): if dep.dependency != new["key"]: frappe.throw(f"Invalid dependency: {new['key']}") if not re.match(r"^\d+\.\d+\.*\d*$", new["value"]): frappe.throw(f"Invalid version for {new['key']}") dep.version = new["value"] rg.save() @jingrow.whitelist() @protected("Release Group") def apps(name): group = frappe.get_pg("Release Group", name) apps = [] deployed_apps = frappe.db.get_all( "Bench", filters={"group": group.name, "status": ("!=", "Archived")}, fields=["`tabBench App`.app"], pluck="app", ) deployed_apps = unique(deployed_apps) updates = deploy_information(name) latest_bench = frappe.get_all( "Bench", filters={"group": group.name, "status": "Active"}, order_by="creation desc", limit=1, pluck="name", ) if latest_bench: latest_bench = latest_bench[0] else: latest_bench = None latest_deployed_apps = frappe.get_all( "Bench", filters={"name": latest_bench}, fields=["`tabBench App`.app", "`tabBench App`.hash"], ) for app in group.apps: source = frappe.get_pg("App Source", app.source) app = frappe.get_pg("App", app.app) update_available = updates["update_available"] and find( updates.apps, lambda x: x["app"] == app.name and x["update_available"] ) latest_deployed_app = find(latest_deployed_apps, lambda x: x.app == app.name) hash = latest_deployed_app.hash if latest_deployed_app else None tag = get_app_tag(source.repository, source.repository_owner, hash) marketplace_app_title = frappe.db.get_value("Marketplace App", app.name, "title") app_title = marketplace_app_title or app.title apps.append( { "name": app.name, "frappe": app.frappe, "title": app_title, "branch": source.branch, "repository_url": source.repository_url, "repository": source.repository, "repository_owner": source.repository_owner, "tag": tag, "hash": hash, "deployed": app.name in deployed_apps, "update_available": bool(update_available), "last_github_poll_failed": source.last_github_poll_failed, } ) return apps @jingrow.whitelist() @protected("Release Group") def installable_apps(name): release_group = frappe.get_pg("Release Group", name) installed_apps = [app.app for app in release_group.apps] versions = options()["versions"] version = find(versions, lambda x: x["name"] == release_group.version) apps = version["apps"] if version else [] return [app for app in apps if app["name"] not in installed_apps] @jingrow.whitelist() @protected("Release Group") def all_apps(name): """Return all apps in the marketplace that are not installed in the release group for adding new apps""" release_group = frappe.get_pg("Release Group", name) installed_apps = [app.app for app in release_group.apps] marketplace_apps = frappe.get_all( "Marketplace App", filters={"status": "Published", "app": ("not in", installed_apps)}, fields=["name", "title", "image", "app"], ) if not marketplace_apps: return [] AppSource = frappe.qb.DocType("App Source") AppSourceVersion = frappe.qb.DocType("App Source Version") marketplace_app_sources = ( frappe.qb.from_(AppSource) .left_join(AppSourceVersion) .on(AppSourceVersion.parent == AppSource.name) .select( AppSource.name, AppSource.branch, AppSource.repository, AppSource.repository_owner, AppSource.app, AppSourceVersion.version, ) .where( (AppSource.app.isin([app.app for app in marketplace_apps])) & (AppSource.enabled == 1) & (AppSource.public == 1) ) ).run(as_dict=1) total_installs_by_app = get_total_installs_by_app() for app in marketplace_apps: app["sources"] = find_all( list(filter(lambda x: x.version == release_group.version, marketplace_app_sources)), lambda x: x.app == app.app, ) # for fetching repo details for incompatible apps app_source = find(marketplace_app_sources, lambda x: x.app == app.app) app["repo"] = f"{app_source.repository_owner}/{app_source.repository}" if app_source else None app["total_installs"] = total_installs_by_app.get(app["name"], 0) return marketplace_apps @jingrow.whitelist() @protected("Release Group") def fetch_latest_app_update(name, app): frappe.get_pg("Release Group", name).fetch_latest_app_update(app) @jingrow.whitelist() @protected("Release Group") def add_app(name, source, app): add_apps(name, [{"app": app, "source": source}]) @jingrow.whitelist() @protected("Release Group") def add_apps(name, apps): release_group: "ReleaseGroup" = frappe.get_pg("Release Group", name) for app in apps: app_name, source = app.values() release_group.update_source(frappe._dict(name=source, app=app_name)) @jingrow.whitelist() @protected("Release Group") def remove_app(name, app): return frappe.get_pg("Release Group", name).remove_app(app) @jingrow.whitelist() @protected("Release Group") def versions(name): Bench = frappe.qb.DocType("Bench") Server = frappe.qb.DocType("Server") deployed_versions = ( frappe.qb.from_(Bench) .left_join(Server) .on(Server.name == Bench.server) .where((Bench.group == name) & (Bench.status != "Archived")) .groupby(Bench.name) .select(Bench.name, Bench.status, Bench.is_ssh_proxy_setup, Server.proxy_server) .orderby(Bench.creation, order=frappe.qb.desc) .run(as_dict=True) ) rg_version = frappe.db.get_value("Release Group", name, "version") sites_in_group_details = frappe.db.get_all( "Site", filters={ "group": name, "status": ("not in", ("Archived", "Suspended")), "is_standby": 0, }, fields=["name", "status", "cluster", "plan", "creation", "bench"], ) if sites_in_group_details: Cluster = frappe.qb.DocType("Cluster") cluster_data = ( frappe.qb.from_(Cluster) .select(Cluster.name, Cluster.title, Cluster.image) .where(Cluster.name.isin([site.cluster for site in sites_in_group_details])) .run(as_dict=True) ) Plan = frappe.qb.DocType("Site Plan") plan_data = ( frappe.qb.from_(Plan) .select(Plan.name, Plan.plan_title, Plan.price_inr, Plan.price_usd) .where(Plan.name.isin([site.plan for site in sites_in_group_details])) .run(as_dict=True) ) ResourceTag = frappe.qb.DocType("Resource Tag") tag_data = ( frappe.qb.from_(ResourceTag) .select(ResourceTag.tag_name, ResourceTag.parent) .where(ResourceTag.parent.isin([site.name for site in sites_in_group_details])) .run(as_dict=True) ) else: cluster_data = plan_data = tag_data = {} for version in deployed_versions: version.sites = find_all(sites_in_group_details, lambda x: x.bench == version.name) version.version = rg_version for site in version.sites: site.version = rg_version site.server_region_info = find(cluster_data, lambda x: x.name == site.cluster) site.plan = find(plan_data, lambda x: x.name == site.plan) tags = find_all(tag_data, lambda x: x.parent == site.name) site.tags = [tag.tag_name for tag in tags] version.deployed_on = frappe.db.get_value( "Agent Job", {"bench": version.name, "job_type": "New Bench", "status": "Success"}, "end", ) return deployed_versions @jingrow.whitelist() @protected("Bench") def get_installed_apps_in_version(name): apps = frappe.db.get_all( "Bench App", {"parent": name}, ["name", "app", "hash", "source"], order_by="idx", ) for app in apps: app.update( frappe.db.get_value( "App Source", app.source, ("branch", "repository", "repository_owner", "repository_url"), as_dict=1, cache=True, ) ) app.tag = get_app_tag(app.repository, app.repository_owner, app.hash) return apps @jingrow.whitelist() @protected("Bench") def get_processes(name): bench: "Bench" = frappe.get_pg("Bench", name) if bench.status != "Active" and bench.status != "Broken": return [] return bench.supervisorctl_status() @jingrow.whitelist() @protected("Release Group") def candidates(filters=None, order_by=None, limit_start=None, limit_page_length=None): # TODO: Status is redundant here. result = frappe.get_all( "Deploy Candidate", ["name", "creation", "status"], {"group": filters["group"], "status": ("!=", "Draft")}, order_by=order_by or "creation desc", start=limit_start, limit=limit_page_length, ) candidates = OrderedDict() for d in result: candidates.setdefault(d.name, {}) candidates[d.name].update(d) dc_apps = frappe.get_all( "Deploy Candidate App", filters={"parent": d.name}, pluck="app", order_by="creation desc", ) candidates[d.name]["apps"] = dc_apps return candidates.values() @jingrow.whitelist() def candidate(name): if not name: return None candidate: DeployCandidate = frappe.get_pg("Deploy Candidate", name) jobs = [] deploys = frappe.get_all("Deploy", {"candidate": name}, limit=1) if deploys: deploy = frappe.get_pg("Deploy", deploys[0].name) for bench in deploy.benches: if not bench.bench: continue job = frappe.get_all( "Agent Job", ["name", "status", "end", "duration", "bench"], {"bench": bench.bench, "job_type": "New Bench"}, limit=1, ) or [{}] jobs.append(job[0]) # Taking the latest Build for that Candidate build_name = frappe.get_value("Deploy Candidate Build", {"deploy_candidate": name}) build: DeployCandidateBuild = frappe.get_pg("Deploy Candidate Build", build_name) return { "name": candidate.name, "status": build.status, "creation": build.creation, "deployed": False, "build_steps": build.build_steps, "build_start": build.build_start, "build_end": build.build_end, "build_duration": build.build_duration, "apps": build.candidate.apps, "jobs": jobs, } @jingrow.whitelist() @protected("Release Group") def deploy_information(name): rg: ReleaseGroup = frappe.get_pg("Release Group", name) return rg.deploy_information() @jingrow.whitelist() @protected("Release Group") def deploy(name, apps): team = get_current_team(True) rg: ReleaseGroup = frappe.get_pg("Release Group", name) if rg.team != team.name: frappe.throw("Bench can only be deployed by the bench owner", exc=frappe.PermissionError) if rg.deploy_in_progress: frappe.throw("A deploy for this bench is already in progress") candidate = rg.create_deploy_candidate(apps) deploy_candidate_build = candidate.schedule_build_and_deploy() return deploy_candidate_build["name"] @jingrow.whitelist() @protected("Release Group") def deploy_and_update( name: str, apps: list, sites: list | None = None, run_will_fail_check: bool = True, ): # Returns name of the Deploy Candidate that is running the build return get_bench_update( name, apps, sites, False, ).deploy(run_will_fail_check) @jingrow.whitelist() @protected("Release Group") def update_inplace( name: str, apps: list, sites: list, ): # Returns name of the Agent Job name that runs the inplace update return get_bench_update( name, apps, sites, True, ).update_inplace() @jingrow.whitelist() @protected("Release Group") def create_deploy_candidate(name, apps_to_ignore=None): apps_to_ignore = [] if apps_to_ignore is None else apps_to_ignore rg: ReleaseGroup = frappe.get_pg("Release Group", name) return rg.create_deploy_candidate(apps_to_ignore) @jingrow.whitelist() @protected("Release Group") def jobs(filters=None, order_by=None, limit_start=None, limit_page_length=None): benches = frappe.get_all("Bench", {"group": filters["name"]}, pluck="name") if benches: jobs = frappe.get_all( "Agent Job", fields=["name", "job_type", "creation", "status", "start", "end", "duration"], filters={"bench": ("in", benches)}, order_by=order_by or "creation desc", start=limit_start, limit=limit_page_length, ignore_ifnull=True, ) for job in jobs: job["status"] = "Pending" if job["status"] == "Undelivered" else job["status"] else: jobs = [] return jobs @jingrow.whitelist() @protected("Release Group") def running_jobs(name): benches = frappe.get_all("Bench", {"group": name}, pluck="name") jobs = frappe.get_all( "Agent Job", filters={"status": ("in", ("Pending", "Running")), "bench": ("in", benches)}, ) return [job_detail(job.name) for job in jobs] @jingrow.whitelist() @protected("Release Group") def recent_deploys(name): return frappe.get_all( "Deploy Candidate", ["name", "creation"], {"group": name, "status": ("!=", "Draft")}, order_by="creation desc", limit=3, ) @jingrow.whitelist() @protected("Release Group") def change_branch(name: str, app: str, to_branch: str): """Switch to `to_branch` for `app` in release group `name`""" rg: ReleaseGroup = frappe.get_pg("Release Group", name) rg.change_app_branch(app, to_branch) @jingrow.whitelist() @protected("Release Group") def branch_list(name: str, app: str) -> list[dict]: """Return a list of git branches available for the `app`""" rg: ReleaseGroup = frappe.get_pg("Release Group", name) app_source = rg.get_app_source(app) installation_id = app_source.github_installation_id repo_owner = app_source.repository_owner repo_name = app_source.repository marketplace_app = frappe.get_all("Marketplace App", filters={"app": app}, pluck="name", limit=1) if marketplace_app and app_source.public and (not belongs_to_current_team(marketplace_app[0])): return get_branches_for_marketplace_app(app, marketplace_app[0], app_source) return branches(repo_owner, repo_name, installation_id) def get_branches_for_marketplace_app(app: str, marketplace_app: str, app_source: AppSource) -> list[dict]: """Return list of branches allowed for this `marketplace` app""" branch_set = set() marketplace_app = frappe.get_pg("Marketplace App", marketplace_app) for marketplace_app_source in marketplace_app.sources: app_source = frappe.get_pg("App Source", marketplace_app_source.source) branch_set.add(app_source.branch) # Also, append public source branches repo_owner = app_source.repository_owner repo_name = app_source.repository public_app_sources = frappe.get_all( "App Source", filters={ "app": app, "repository_owner": repo_owner, "repository": repo_name, "public": True, }, pluck="branch", ) branch_set.update(public_app_sources) branch_list = sorted(list(branch_set)) return [{"name": b} for b in branch_list] def belongs_to_current_team(app: str) -> bool: """Does the Marketplace App `app` belong to current team""" current_team = get_current_team() marketplace_app = frappe.get_pg("Marketplace App", app) return marketplace_app.team == current_team @jingrow.whitelist() @protected("Release Group") def regions(name): rg = frappe.get_pg("Release Group", name) cluster_names = rg.get_clusters() return frappe.get_all( "Cluster", fields=["name", "title", "image"], filters={"name": ("in", cluster_names)} ) @jingrow.whitelist() @protected("Release Group") def available_regions(name): rg = frappe.get_pg("Release Group", name) cluster_names = rg.get_clusters() return Cluster.get_all_for_new_bench({"name": ("not in", cluster_names)}) @jingrow.whitelist() @protected("Release Group") def add_region(name, region): frappe.get_pg("Release Group", name).add_region(region) @jingrow.whitelist() @protected("Release Group") def archive(name): benches = frappe.get_all("Bench", filters={"group": name, "status": "Active"}, pluck="name") for bench in benches: frappe.get_pg("Bench", bench).archive() group = frappe.get_pg("Release Group", name) new_name = f"{group.title}.archived" group.title = append_number_if_name_exists("Release Group", new_name, "title", separator=".") group.enabled = 0 group.save() @jingrow.whitelist() @protected("Bench") def restart(name): frappe.get_pg("Bench", name).restart() @jingrow.whitelist() @protected("Bench") def rebuild(name): frappe.get_pg("Bench", name).rebuild() @jingrow.whitelist() @protected("Bench") def update(name): frappe.get_pg("Bench", name).update_all_sites() @jingrow.whitelist() @protected("Release Group") def update_all_sites(name): benches = frappe.get_all("Bench", {"group": name, "status": "Active"}) for bench in benches: frappe.get_cached_pg("Bench", bench).update_all_sites() @jingrow.whitelist() @protected("Release Group") def logs(name, bench): from press.agent import AgentRequestSkippedException if frappe.db.get_value("Bench", bench, "group") != name: return [] try: return frappe.get_pg("Bench", bench).server_logs except AgentRequestSkippedException: return [] @jingrow.whitelist() @protected("Release Group") def log(name, bench, log): if frappe.db.get_value("Bench", bench, "group") != name: frappe.throw(f"Release Group name {name} does not match Bench Release Group") return frappe.get_pg("Bench", bench).get_server_log(log) @jingrow.whitelist() @protected("Release Group") def certificate(name): return frappe.get_pg("Release Group", name).get_certificate() @jingrow.whitelist() @protected("Release Group") def generate_certificate(name): return frappe.get_pg("Release Group", name).generate_certificate() @jingrow.whitelist() @protected("Release Group") def get_title_and_creation(name): result = frappe.db.get_value("Release Group", name, ["title", "creation"], as_dict=True) server = frappe.get_all( "Release Group Server", {"parent": name}, pluck="server", order_by="idx asc", limit=1 )[0] result["team"] = frappe.db.get_value("Server", server, "team") return result @jingrow.whitelist() @protected("Release Group") def rename(name, title): return frappe.db.set_value("Release Group", name, "title", title) @jingrow.whitelist() @protected("Release Group") def apply_patch(release_group: str, app: str, patch_config: dict) -> list[str]: team = get_current_team() return create_app_patch( release_group, app, team, patch_config, ) @jingrow.whitelist() @protected("Release Group") def fail_build(dn: str): failed = fail_remote_job(dn) if not failed: frappe.throw("No running job found!") @jingrow.whitelist() @protected("Release Group") def fail_and_redeploy(name: str, dc_name: str): res = fail_and_redeploy_build(dc_name) # If failed error is True if res.get("error"): return None # New Deploy Candidate name return res.get("message") @jingrow.whitelist() @protected("Release Group") def show_app_versions(name: str, dc_name: str) -> dict[str, str]: """Get app versions from the deploy candidate""" candidate = frappe.db.get_value("Deploy Candidate Build", dc_name, "deploy_candidate") deploy_candidate: "DeployCandidate" = frappe.get_cached_pg("Deploy Candidate", candidate) app_sources = frappe.db.get_all( "App Source", {"name": ("IN", [app.source for app in deploy_candidate.apps])}, ["name", "branch", "repository", "repository_owner", "repository_url"], ) sources = { item["name"]: { "branch": item["branch"], "repository_url": item["repository_url"], "repository": item["repository"], "repository_owner": item["repository_owner"], } for item in app_sources } return [ { "name": app.app, "hash": app.hash[:7], "branch": sources.get(app.source).get("branch"), "repository": sources.get(app.source).get("repository"), "repository_owner": sources.get(app.source).get("repository_owner"), "repository_url": sources.get(app.source).get("repository_url"), } for app in deploy_candidate.apps ] @jingrow.whitelist() @protected("Release Group") def redeploy(name: str, dc_name: str) -> str: response = redeploy_candidate(dc_name) if response["error"]: frappe.throw("Unable to redeploy this build!", frappe.ValidationError) return response["message"] @jingrow.whitelist(allow_guest=True) def confirm_bench_transfer(key: str): from frappe import _ if frappe.session.user == "Guest": return frappe.respond_as_web_page( _("Not Permitted"), _("You need to be logged in to confirm the bench group transfer."), http_status_code=403, indicator_color="red", primary_action="/dashboard/login", primary_label=_("Login"), ) if not isinstance(key, str): return frappe.respond_as_web_page( _("Not Permitted"), _("The link you are using is invalid."), http_status_code=403, indicator_color="red", ) if team_change := frappe.db.get_value("Team Change", {"key": key}): team_change = frappe.get_pg("Team Change", team_change) to_team = team_change.to_team if not frappe.db.get_value( "Team Member", {"user": frappe.session.user, "parent": to_team, "parenttype": "Team"} ): return frappe.respond_as_web_page( _("Not Permitted"), _("You are not a member of the team to which the site is being transferred."), http_status_code=403, indicator_color="red", ) team_change.transfer_completed = True team_change.save() frappe.db.commit() frappe.response.type = "redirect" frappe.response.location = f"/dashboard/groups/{team_change.document_name}" return None return frappe.respond_as_web_page( _("Not Permitted"), _("The link you are using is invalid or expired."), http_status_code=403, indicator_color="red", )