From 2495123446541cfb968c3bee3375c136ef42f778 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Wed, 13 Dec 2023 12:50:26 -0600 Subject: [PATCH] Deploy (and update) from registry. --- stack_orchestrator/deploy/deployment.py | 13 - .../deploy/webapp/deploy_webapp.py | 47 ++-- .../webapp/deploy_webapp_from_registry.py | 259 ++++++++++++++++++ .../deploy/webapp/update_from_registry.py | 160 ----------- stack_orchestrator/deploy/webapp/util.py | 246 +++++++++++++++++ stack_orchestrator/main.py | 3 +- 6 files changed, 533 insertions(+), 195 deletions(-) create mode 100644 stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py delete mode 100644 stack_orchestrator/deploy/webapp/update_from_registry.py create mode 100644 stack_orchestrator/deploy/webapp/util.py diff --git a/stack_orchestrator/deploy/deployment.py b/stack_orchestrator/deploy/deployment.py index be51dd6d..f364121f 100644 --- a/stack_orchestrator/deploy/deployment.py +++ b/stack_orchestrator/deploy/deployment.py @@ -22,7 +22,6 @@ from stack_orchestrator.deploy.deploy import up_operation, down_operation, ps_op from stack_orchestrator.deploy.deploy import exec_operation, logs_operation, create_deploy_context, update_operation from stack_orchestrator.deploy.deploy_types import DeployCommandContext from stack_orchestrator.deploy.deployment_context import DeploymentContext -from stack_orchestrator.deploy.webapp import update_from_registry as webapp_update @click.group() @@ -152,18 +151,6 @@ def status(ctx): status_operation(ctx) -@command.command() -@click.option("--laconic-config", help="laconic-cli config file", required=True) -# TODO: Add to spec.yml? -@click.option("--app-crn", help="application CRN", required=True) -@click.option("--deployment-crn", help="webapp deployment CRN", required=True) -@click.option("--force", help="force redeployment", is_flag=True, default=False) -@click.pass_context -def update_from_registry(ctx, laconic_config, app_crn, deployment_crn, force): - ctx.obj = make_deploy_context(ctx) - webapp_update.update(ctx, str(ctx.obj.stack.parent), laconic_config, app_crn, deployment_crn, force) - - @command.command() @click.pass_context def update(ctx): diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp.py b/stack_orchestrator/deploy/webapp/deploy_webapp.py index 391162c9..dbcb2a5e 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp.py @@ -54,26 +54,7 @@ def _fixup_url_spec(spec_file_name: str, url: str): wfile.write(contents) -@click.group() -@click.pass_context -def command(ctx): - '''manage a webapp deployment''' - - # Check that --stack wasn't supplied - if ctx.parent.obj.stack: - error_exit("--stack can't be supplied with the deploy-webapp command") - - -@command.command() -@click.option("--kube-config", help="Provide a config file for a k8s deployment") -@click.option("--image-registry", help="Provide a container image registry url for this k8s cluster") -@click.option("--deployment-dir", help="Create deployment files in this directory", required=True) -@click.option("--image", help="image to deploy", required=True) -@click.option("--url", help="url to serve", required=True) -@click.option("--env-file", help="environment file for webapp") -@click.pass_context -def create(ctx, deployment_dir, image, url, kube_config, image_registry, env_file): - '''create a deployment for the specified webapp container''' +def create_deployment(ctx, deployment_dir, image, url, kube_config, image_registry, env_file): # Do the equivalent of: # 1. laconic-so --stack webapp-template deploy --deploy-to k8s init --output webapp-spec.yml # --config (eqivalent of the contents of my-config.env) @@ -92,7 +73,7 @@ def create(ctx, deployment_dir, image, url, kube_config, image_registry, env_fil # TODO: support env file deploy_command_context: DeployCommandContext = create_deploy_context( global_options2(ctx), None, stack, None, None, None, env_file, "k8s" - ) + ) init_operation( deploy_command_context, stack, @@ -116,3 +97,27 @@ def create(ctx, deployment_dir, image, url, kube_config, image_registry, env_fil # Fix up the container tag inside the deployment compose file _fixup_container_tag(deployment_dir, image) os.remove(spec_file_name) + + +@click.group() +@click.pass_context +def command(ctx): + '''manage a webapp deployment''' + + # Check that --stack wasn't supplied + if ctx.parent.obj.stack: + error_exit("--stack can't be supplied with the deploy-webapp command") + + +@command.command() +@click.option("--kube-config", help="Provide a config file for a k8s deployment") +@click.option("--image-registry", help="Provide a container image registry url for this k8s cluster") +@click.option("--deployment-dir", help="Create deployment files in this directory", required=True) +@click.option("--image", help="image to deploy", required=True) +@click.option("--url", help="url to serve", required=True) +@click.option("--env-file", help="environment file for webapp") +@click.pass_context +def create(ctx, deployment_dir, image, url, kube_config, image_registry, env_file): + '''create a deployment for the specified webapp container''' + + return create_deployment(ctx, deployment_dir, image, url, kube_config, image_registry, env_file) \ No newline at end of file diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py new file mode 100644 index 00000000..4a805239 --- /dev/null +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -0,0 +1,259 @@ +# Copyright ©2023 Vulcanize + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import hashlib +import json +import os +import shlex +import shutil +import sys +import tempfile + +import click + +from stack_orchestrator.deploy.webapp import deploy_webapp +from stack_orchestrator.deploy.webapp.util import (AttrDict, LaconicRegistryClient, + build_container_image, push_container_image, + file_hash, deploy_to_k8s, publish_deployment) + + +def process_app_deployment_request( + ctx, + laconic: LaconicRegistryClient, + app_deployment_request, + deployment_record_namespace, + dns_record_namespace, + deployment_parent_dir, + kube_config, + image_registry +): + # 1. look up application + app = laconic.get_record(app_deployment_request.attributes.application, require=True) + + # 2. determine dns + requested_name = hostname_for_deployment_request(app_deployment_request, laconic) + # HACK + if "." in requested_name: + raise Exception("Only unqualified hostnames allowed at this time.") + + # HACK + fqdn = f"{requested_name}.laconic.servesthe.world" + container_tag = "%s:local" % app.attributes.name.replace("@", "") + + # 3. check ownership of existing dnsrecord vs this request + + # 4. get build and runtime config from request + env_filename = None + if app_deployment_request.attributes.config and "env" in app_deployment_request.attributes.config: + env_filename = tempfile.mktemp() + with open(env_filename, 'w') as file: + for k, v in app_deployment_request.attributes.config["env"].items(): + file.write("%s=%s\n" % (k, shlex.quote(str(v)))) + + # 5. determine new or existing deployment + # a. check for deployment crn + app_deployment_crn = f"{deployment_record_namespace}/{fqdn}" + if app_deployment_request.attributes.deployment: + app_deployment_crn = app_deployment_request.attributes.deployment + if not app_deployment_crn.startswith(deployment_record_namespace): + raise Exception("Deployment CRN %s is not in a supported namespace" % app_deployment_request.attributes.deployment) + + deployment_record = laconic.get_record(app_deployment_crn) + deployment_dir = os.path.join(deployment_parent_dir, fqdn) + deployment_config_file = os.path.join(deployment_dir, "config.env") + # b. check for deployment directory (create if necessary) + if not os.path.exists(deployment_dir): + if deployment_record: + raise ("Deployment record %s exists, but not deployment dir %s. Please remove name." % + (app_deployment_crn, deployment_dir)) + print("deploy_webapp", deployment_dir) + deploy_webapp.create_deployment(ctx, deployment_dir, container_tag, + f"https://{fqdn}", kube_config, image_registry, env_filename) + elif env_filename: + shutil.copyfile(env_filename, deployment_config_file) + + needs_k8s_deploy = False + # 6. build container (if needed) + if not deployment_record or deployment_record.attributes.application != app.id: + build_container_image(app, container_tag) + push_container_image(deployment_dir) + needs_k8s_deploy = True + + # 7. update config (if needed) + if not deployment_record or file_hash(deployment_config_file) != deployment_record.attributes.meta.config: + needs_k8s_deploy = True + + # 8. update k8s deployment + if needs_k8s_deploy: + print("Deploying to k8s") + deploy_to_k8s( + laconic, + app, + deployment_record, + app_deployment_crn, + deployment_dir, + app_deployment_request + ) + + publish_deployment( + laconic, + app, + deployment_record, + app_deployment_crn, + deployment_dir, + app_deployment_request + ) + + +def hostname_for_deployment_request(app_deployment_request, laconic): + dns_name = app_deployment_request.attributes.dns + if not dns_name: + app = laconic.get_record(app_deployment_request.attributes.application, require=True) + dns_name = generate_hostname_for_app(app) + elif dns_name.startswith("crn://"): + record = laconic.get_record(dns_name, require=True) + dns_name = record.attributes.name + return dns_name + + +def generate_hostname_for_app(app): + last_part = app.attributes.name.split("/")[-1] + m = hashlib.sha256() + m.update(app.attributes.name.encode()) + m.update(b"|") + if isinstance(app.attributes.repository, list): + m.update(app.attributes.repository[0].encode()) + else: + m.update(app.attributes.repository.encode()) + return "%s-%s" % (last_part, m.hexdigest()[0:10]) + + +def load_known_requests(filename): + if filename and os.path.exists(filename): + return json.load(open(filename, "r")) + return {} + + +def dump_known_requests(filename, requests): + if not filename: + return + known_requests = load_known_requests(filename) + for r in requests: + known_requests[r.id] = r.createTime + json.dump(known_requests, open(filename, "w")) + + +@click.command() +@click.option("--kube-config", help="Provide a config file for a k8s deployment") +@click.option("--laconic-config", help="Provide a config file for laconicd", required=True) +@click.option("--image-registry", help="Provide a container image registry url for this k8s cluster") +@click.option("--deployment-parent-dir", help="Create deployment directories beneath this directory", required=True) +@click.option("--request-id", help="The ApplicationDeploymentRequest to process") +@click.option("--discover", help="Discover and process all pending ApplicationDeploymentRequests", is_flag=True, default=False) +@click.option("--state-file", help="File to store state about previously seen requests.") +@click.option("--only-update-state", help="Only update the state file, don't process any requests anything.", is_flag=True) +@click.pass_context +def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_dir, request_id, discover, state_file, only_update_state): + if request_id and discover: + print("Cannot specify both --request-id and --discover", file=sys.stderr) + sys.exit(2) + + if not request_id and not discover: + print("Must specify either --request-id or --discover", file=sys.stderr) + sys.exit(2) + + if only_update_state and not state_file: + print("--only-update-state requires --state-file", file=sys.stderr) + sys.exit(2) + + laconic = LaconicRegistryClient(laconic_config) + + # Find deployment requests. + # single request + if request_id: + requests = [laconic.get_record(request_id, require=True)] + # all requests + elif discover: + requests = laconic.app_deployment_requests() + + if only_update_state: + dump_known_requests(state_file, requests) + return + + previous_requests = load_known_requests(state_file) + + # Collapse related requests. + requests.sort(key=lambda r: r.createTime) + requests.reverse() + requests_by_name = {} + for r in requests: + app = laconic.get_record(r.attributes.application) + if not app: + print("Skipping request %s, cannot locate app." % r.id) + continue + + requested_name = r.attributes.dns + if not requested_name: + requested_name = generate_hostname_for_app(app) + print("Generating name %s for request %s." % (requested_name, r.id)) + + if requested_name not in requests_by_name: + print( + "Found request %s to run application %s on %s." + % (r.id, r.attributes.application, requested_name) + ) + requests_by_name[requested_name] = r + else: + print( + "Ignoring request %s, it is superseded by %s." + % (r.id, requests_by_name[requested_name].id) + ) + + # Find deployments. + deployments = laconic.app_deployments() + deployments_by_request = {} + for d in deployments: + if d.attributes.request: + deployments_by_request[d.attributes.request] = d + + requests_to_execute = [] + for r in requests_by_name.values(): + if r.id not in deployments_by_request: + if r.id not in previous_requests: + print(f"Request {r.id} needs to processed.") + requests_to_execute.append(r) + else: + print( + f"Skipping unsatisfied request {r.id} because we have seen it before." + ) + else: + print(f"Found satisfied request {r.id}") + + print("Found %d unsatisfied request(s) to process." % len(requests_to_execute)) + + for r in requests_to_execute: + try: + process_app_deployment_request( + ctx, + laconic, + r, + "crn://cerc-io/deployments", + "crn://cerc-io/dns", + deployment_parent_dir, + kube_config, + image_registry + ) + finally: + dump_known_requests(state_file, [r]) \ No newline at end of file diff --git a/stack_orchestrator/deploy/webapp/update_from_registry.py b/stack_orchestrator/deploy/webapp/update_from_registry.py deleted file mode 100644 index 0ceb1395..00000000 --- a/stack_orchestrator/deploy/webapp/update_from_registry.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright © 2023 Vulcanize - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import hashlib -import json -import os -import subprocess -import sys -import tempfile - -import yaml - - -def cmd(*vargs): - try: - result = subprocess.run(vargs, capture_output=True) - result.check_returncode() - return result.stdout.decode() - except Exception as err: - print(result.stderr.decode()) - raise err - - -def build_image(app_record, deployment_dir): - tmpdir = tempfile.mkdtemp() - - try: - record_id = app_record["id"] - name = app_record.get("attributes", {})["name"].replace("@", "") - tag = app_record.get("attributes", {}).get("repository_tag") - repo = app_record.get("attributes", {}).get("repository") - clone_dir = os.path.join(tmpdir, record_id) - - print(f"Cloning repository {repo} to {clone_dir} ...") - if tag: - result = subprocess.run(["git", "clone", "--depth", "1", "--branch", tag, repo, clone_dir]) - result.check_returncode() - else: - result = subprocess.run(["git", "clone", "--depth", "1", repo, clone_dir]) - result.check_returncode() - - print("Building webapp ...") - result = subprocess.run([sys.argv[0], "build-webapp", "--source-repo", clone_dir, "--tag", f"{name}:local"]) - result.check_returncode() - - print("Pushing image ...") - result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, "push-images"]) - result.check_returncode() - finally: - cmd("rm", "-rf", tmpdir) - - -def config_hash(deployment_dir): - return hashlib.sha1(open(os.path.join(deployment_dir, "config.env")).read().encode()).hexdigest() - - -def config_changed(deploy_record, deployment_dir): - if not deploy_record: - return True - old = json.loads(deploy_record["attributes"]["meta"])["config"] - return config_hash(deployment_dir) != old - - -def redeploy(laconic_config, app_record, deploy_record, deploy_crn, deployment_dir): - print("Updating deployment ...") - result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, "update"]) - result.check_returncode() - - spec = yaml.full_load(open(os.path.join(deployment_dir, "spec.yml"))) - hostname = spec["network"]["http-proxy"][0]["host-name"] - - if not deploy_record: - version = "0.0.1" - else: - version = "0.0.%d" % (int(deploy_record["attributes"]["version"].split(".")[-1]) + 1) - - meta = { - "record": { - "type": "WebAppDeploymentRecord", - "version": version, - "url": f"http://{hostname}", - "name": hostname, - "application": app_record["id"], - "meta": { - "config": config_hash(deployment_dir) - }, - } - } - - tmpdir = tempfile.mkdtemp() - try: - record_fname = os.path.join(tmpdir, "record.yml") - record_file = open(record_fname, 'w') - yaml.dump(meta, record_file) - record_file.close() - print(open(record_fname, 'r').read()) - - print("Updating deployment record ...") - new_record_id = json.loads( - cmd("laconic", "-c", laconic_config, "cns", "record", "publish", "--filename", record_fname) - )["id"] - print("Updating deployment registered name ...") - cmd("laconic", "-c", laconic_config, "cns", "name", "set", deploy_crn, new_record_id) - finally: - cmd("rm", "-rf", tmpdir) - - -def update(ctx, deployment_dir, laconic_config, app_crn, deploy_crn, force=False): - '''update the specified webapp deployment''' - - # The deployment must already exist - if not os.path.exists(deployment_dir): - print("Deployment does not exist:", deployment_dir, file=sys.stderr) - sys.exit(1) - - # resolve name - app_record = json.loads(cmd("laconic", "-c", laconic_config, "cns", "name", "resolve", app_crn))[0] - - # compare - try: - deploy_record = json.loads(cmd("laconic", "-c", laconic_config, "cns", "name", "resolve", deploy_crn))[0] - except: # noqa: E722 - deploy_record = {} - - needs_update = False - - if app_record["id"] == deploy_record.get("attributes", {}).get("application"): - print("Deployment %s has latest application: %s" % (deploy_crn, app_record["id"])) - else: - needs_update = True - print("Found updated application record eligible for deployment %s (old: %s, new: %s)" % ( - deploy_crn, deploy_record.get("id"), app_record["id"])) - build_image(app_record, deployment_dir) - - # check config - if config_changed(deploy_record, deployment_dir): - needs_update = True - old = None - if deploy_record: - old = json.loads(deploy_record["attributes"]["meta"])["config"] - print("Deployment %s has changed config: (old: %s, new: %s)" % ( - deploy_crn, old, config_hash(deployment_dir))) - else: - print("Deployment %s has latest config: %s" % ( - deploy_crn, json.loads(deploy_record["attributes"]["meta"])["config"])) - - if needs_update or force: - redeploy(laconic_config, app_record, deploy_record, deploy_crn, deployment_dir) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py new file mode 100644 index 00000000..4886626f --- /dev/null +++ b/stack_orchestrator/deploy/webapp/util.py @@ -0,0 +1,246 @@ +# Copyright © 2023 Vulcanize + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import hashlib +import json +import os +import random +import subprocess +import sys +import tempfile +import yaml + + +class AttrDict(dict): + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self + + def __getattribute__(self, attr): + __dict__ = super(AttrDict, self).__getattribute__("__dict__") + if attr in __dict__: + v = super(AttrDict, self).__getattribute__(attr) + if isinstance(v, dict): + return AttrDict(v) + return v + + +def cmd(*vargs): + try: + result = subprocess.run(vargs, capture_output=True) + result.check_returncode() + return result.stdout.decode() + except Exception as err: + print(result.stderr.decode()) + raise err + + +class LaconicRegistryClient: + def __init__(self, config_file): + self.config_file = config_file + self.cache = AttrDict( + { + "name_or_id": {}, + } + ) + + def list_records(self, criteria={}, all=False): + args = ["laconic", "-c", self.config_file, "cns", "record", "list"] + + if all: + args.append("--all") + + if criteria: + for k, v in criteria.items(): + args.append("--%s" % k) + args.append(str(v)) + + results = [AttrDict(r) for r in json.loads(cmd(*args))] + + # Most recent records first + results.sort(key=lambda r: r.createTime) + results.reverse() + + return results + + def is_crn(self, name_or_id: str): + if name_or_id: + return str(name_or_id).startswith("crn://") + return False + + def is_id(self, name_or_id: str): + return not self.is_crn(name_or_id) + + def _add_to_cache(self, records): + if not records: + return + + for p in records: + self.cache["name_or_id"][p.id] = p + if p.names: + for crn in p.names: + self.cache["name_or_id"][crn] = p + if p.attributes.type not in self.cache: + self.cache[p.attributes.type] = [] + self.cache[p.attributes.type].append(p) + + def resolve(self, name): + if not name: + return None + + if name in self.cache.name_or_id: + return self.cache.name_or_id[name] + + args = ["laconic", "-c", self.config_file, "cns", "name", "resolve", name] + + parsed = [AttrDict(r) for r in json.loads(cmd(*args))] + if parsed: + self._add_to_cache(parsed) + return parsed[0] + + return None + + def get_record(self, name_or_id, require=False): + if not name_or_id: + if require: + raise Exception("Cannot locate record:", name_or_id) + return None + + if name_or_id in self.cache.name_or_id: + return self.cache.name_or_id[name_or_id] + + if self.is_crn(name_or_id): + return self.resolve(name_or_id) + + args = [ + "laconic", + "-c", + self.config_file, + "cns", + "record", + "get", + "--id", + name_or_id, + ] + + parsed = [AttrDict(r) for r in json.loads(cmd(*args))] + if len(parsed): + self._add_to_cache(parsed) + return parsed[0] + + if require: + raise Exception("Cannot locate record:", name_or_id) + return None + + def app_deployment_requests(self): + return self.list_records({"type": "ApplicationDeploymentRequest"}, True) + + def app_deployments(self): + return self.list_records({"type": "ApplicationDeploymentRecord"}) + + def publish(self, record, names=[]): + tmpdir = tempfile.mkdtemp() + try: + record_fname = os.path.join(tmpdir, "record.yml") + record_file = open(record_fname, 'w') + yaml.dump(record, record_file) + record_file.close() + print(open(record_fname, 'r').read()) + + new_record_id = json.loads( + cmd("laconic", "-c", self.config_file, "cns", "record", "publish", "--filename", record_fname) + )["id"] + for name in names: + cmd("laconic", "-c", self.config_file, "cns", "name", "set", name, new_record_id) + return new_record_id + finally: + cmd("rm", "-rf", tmpdir) + + +def file_hash(filename): + return hashlib.sha1(open(filename).read().encode()).hexdigest() + + +def build_container_image(app_record, tag, extra_build_args=[]): + tmpdir = tempfile.mkdtemp() + + try: + record_id = app_record["id"] + name = app_record.attributes.name.replace("@", "") + ref = app_record.attributes.repository_ref + repo = random.choice(app_record.attributes.repository) + clone_dir = os.path.join(tmpdir, record_id) + + print(f"Cloning repository {repo} to {clone_dir} ...") + if ref: + result = subprocess.run(["git", "clone", "--depth", "1", "--branch", ref, repo, clone_dir]) + result.check_returncode() + else: + result = subprocess.run(["git", "clone", "--depth", "1", repo, clone_dir]) + result.check_returncode() + + print("Building webapp ...") + build_command = [sys.argv[0], "build-webapp", "--source-repo", clone_dir, "--tag", tag] + if extra_build_args: + build_command.append("--extra-build-args") + build_command.append(" ".join(extra_build_args)) + + result = subprocess.run(build_command) + result.check_returncode() + finally: + cmd("rm", "-rf", tmpdir) + + +def push_container_image(deployment_dir): + print("Pushing image ...") + result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, "push-images"]) + result.check_returncode() + + +def deploy_to_k8s(laconic: LaconicRegistryClient, app_record, deploy_record, deployment_crn, deployment_dir, app_deployment_request=None): + if not deploy_record: + command = "up" + else: + command = "update" + + result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, command]) + result.check_returncode() + + +def publish_deployment(laconic: LaconicRegistryClient, app_record, deploy_record, deployment_crn, deployment_dir, app_deployment_request=None): + if not deploy_record: + version = "0.0.1" + else: + version = "0.0.%d" % (int(deploy_record["attributes"]["version"].split(".")[-1]) + 1) + + spec = yaml.full_load(open(os.path.join(deployment_dir, "spec.yml"))) + hostname = spec["network"]["http-proxy"][0]["host-name"] + + record = { + "record": { + "type": "ApplicationDeploymentRecord", + "version": version, + "url": f"https://{hostname}", + "name": hostname, + "application": app_record["id"], + "meta": { + "config": file_hash(os.path.join(deployment_dir, "config.env")) + }, + } + } + if app_deployment_request: + record["record"]["request"] = app_deployment_request.id + + return laconic.publish(record, [deployment_crn]) diff --git a/stack_orchestrator/main.py b/stack_orchestrator/main.py index 26a011b0..fabc58ff 100644 --- a/stack_orchestrator/main.py +++ b/stack_orchestrator/main.py @@ -20,7 +20,7 @@ from stack_orchestrator.repos import setup_repositories from stack_orchestrator.build import build_containers from stack_orchestrator.build import build_npms from stack_orchestrator.build import build_webapp -from stack_orchestrator.deploy.webapp import run_webapp, deploy_webapp +from stack_orchestrator.deploy.webapp import run_webapp, deploy_webapp, deploy_webapp_from_registry from stack_orchestrator.deploy import deploy from stack_orchestrator import version from stack_orchestrator.deploy import deployment @@ -53,6 +53,7 @@ cli.add_command(build_npms.command, "build-npms") cli.add_command(build_webapp.command, "build-webapp") cli.add_command(run_webapp.command, "run-webapp") cli.add_command(deploy_webapp.command, "deploy-webapp") +cli.add_command(deploy_webapp_from_registry.command, "deploy-webapp-from-registry") cli.add_command(deploy.command, "deploy") # deploy is an alias for deploy-system cli.add_command(deploy.command, "deploy-system") cli.add_command(deployment.command, "deployment")