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")