From f3e41629dbbb61baf0c6adc89a892d239cf80e04 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Fri, 1 Dec 2023 00:22:34 -0600 Subject: [PATCH] WIP: Auto-redeploy webapps based on laconicd records. --- .../auto-deployer/auto-deploy-webapp.py | 146 ++++++++++++++++++ .../scripts/auto-deployer/requirements.txt | 1 + 2 files changed, 147 insertions(+) create mode 100755 stack_orchestrator/data/config/nextjs-base/scripts/auto-deployer/auto-deploy-webapp.py create mode 100644 stack_orchestrator/data/config/nextjs-base/scripts/auto-deployer/requirements.txt diff --git a/stack_orchestrator/data/config/nextjs-base/scripts/auto-deployer/auto-deploy-webapp.py b/stack_orchestrator/data/config/nextjs-base/scripts/auto-deployer/auto-deploy-webapp.py new file mode 100755 index 00000000..f7629ead --- /dev/null +++ b/stack_orchestrator/data/config/nextjs-base/scripts/auto-deployer/auto-deploy-webapp.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 + +import argparse +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(["laconic-so", "build-webapp", "--source-repo", clone_dir, "--tag", f"{name}:local"]) + result.check_returncode() + + print("Pushing image ...") + result = subprocess.run(["laconic-so", "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(app_record, deploy_record, deploy_crn, deployment_dir): + print("Stopping deployment ...") + result = subprocess.run(["laconic-so", "deployment", "--dir", deployment_dir, "stop"]) + result.check_returncode() + + print("Starting deployment ...") + result = subprocess.run(["laconic-so", "deployment", "--dir", deployment_dir, "start"]) + 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", args.laconic_config, "cns", "record", "publish", "--filename", record_fname))[ + "id"] + print("Updating deployment registered name ...") + cmd("laconic", "-c", args.laconic_config, "cns", "name", "set", deploy_crn, new_record_id) + finally: + cmd("rm", "-rf", tmpdir) + + +parser = argparse.ArgumentParser() +parser.add_argument("--laconic-config", required=True) +parser.add_argument("--app-crn", required=True) +parser.add_argument("--deploy-crn", required=True) +parser.add_argument("--deployment-dir", required=True) +parser.add_argument("--force", action="store_true") +args = parser.parse_args() + +# The deployment must already exist +if not os.path.exists(args.deployment_dir): + print("Deployment does not exist:", args.deployment_dir, file=sys.stderr) + sys.exit(1) + +# resolve name +app_record = json.loads(cmd("laconic", "-c", args.laconic_config, "cns", "name", "resolve", args.app_crn))[0] + +# compare +try: + deploy_record = json.loads(cmd("laconic", "-c", args.laconic_config, "cns", "name", "resolve", args.deploy_crn))[0] +except: + deploy_record = {} + +needs_update = False + +if app_record["id"] == deploy_record.get("attributes", {}).get("application"): + print("Deployment %s already has latest application: %s" % (args.deploy_crn, app_record["id"])) +else: + print("Found updated application record eligible for deployment %s (old: %s, new: %s)" % ( + args.deploy_crn, deploy_record.get("id"), app_record["id"])) + build_image(app_record, args.deployment_dir) + needs_update = True + +# check config +if config_changed(deploy_record, args.deployment_dir): + old = json.loads(deploy_record["attributes"]["meta"])["config"] + print("Deployment %s has updated config: (old: %s, new: %s)" % ( + args.deploy_crn, old, config_hash(args.deployment_dir))) + needs_update = True +else: + print("Deployment %s already has latest config: %s" % ( + args.deploy_crn, json.loads(deploy_record["attributes"]["meta"])["config"])) + +if needs_update or args.force: + redeploy(app_record, deploy_record, args.deploy_crn, args.deployment_dir) diff --git a/stack_orchestrator/data/config/nextjs-base/scripts/auto-deployer/requirements.txt b/stack_orchestrator/data/config/nextjs-base/scripts/auto-deployer/requirements.txt new file mode 100644 index 00000000..11b65037 --- /dev/null +++ b/stack_orchestrator/data/config/nextjs-base/scripts/auto-deployer/requirements.txt @@ -0,0 +1 @@ +PyYAML>=6.0.1