From 63da907b3f54b1587269bc150de9eda42c0ecd80 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Thu, 21 Dec 2023 16:13:54 -0600 Subject: [PATCH] Working --- .../webapp/undeploy_webapp_from_registry.py | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py diff --git a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py new file mode 100644 index 00000000..b79f4681 --- /dev/null +++ b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py @@ -0,0 +1,169 @@ +# 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 json +import os +import shutil +import subprocess +import sys + +import click + +from stack_orchestrator.deploy.webapp.util import LaconicRegistryClient, cmd + + +def process_app_removal_request(ctx, laconic: LaconicRegistryClient, app_removal_request, deployment_parent_dir, delete_volumes, delete_names): + deployment_record = laconic.get_record(app_removal_request.attributes.deployment, require=True) + dns_record = laconic.get_record(deployment_record.attributes.dns, require=True) + deployment_dir = os.path.join(deployment_parent_dir, dns_record.attributes.name) + + if not os.path.exists(deployment_dir): + raise Exception("Deployment directory %s does not exist." % deployment_dir) + + # TODO(telackey): Call the function directly. The easiest way to build the correct click context is to + # exec the process, but it would be better to refactor so we could just call down_operation with the + # necessary parameters + down_command = [sys.argv[0], "deployment", "--dir", deployment_dir, "down"] + if delete_volumes: + down_command.append("--delete-volumes") + result = subprocess.run(down_command) + result.check_returncode() + + removal_record = { + "record": { + "type": "ApplicationDeploymentRemovalRecord", + "version": "1.0.0", + "request": app_removal_request.id, + "deployment": deployment_record.id, + } + } + laconic.publish(removal_record) + + if delete_names: + if deployment_record.names: + for name in deployment_record.names: + laconic.delete_name(name) + + if dns_record.names: + for name in dns_record.names: + laconic.delete_name(name) + + if delete_volumes: + shutil.rmtree(deployment_dir) + + +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("--laconic-config", help="Provide a config file for laconicd", required=True) +@click.option("--deployment-parent-dir", help="Create deployment directories beneath this directory", required=True) +@click.option("--request-id", help="The ApplicationDeploymentRemovalRequest to process") +@click.option("--discover", help="Discover and process all pending ApplicationDeploymentRemovalRequests", 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.option("--delete-names/--preserve-names", help="Delete all names associated with removed deployments.", default=True) +@click.option("--delete-volumes/--preserve-volumes", default=True, help="delete data volumes") +@click.option("--dry-run", help="Don't do anything, just report what would be done.", is_flag=True) +@click.pass_context +def command(ctx, laconic_config, deployment_parent_dir, + request_id, discover, state_file, only_update_state, + delete_names, delete_volumes, dry_run): + 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 removal requests. + # single request + if request_id: + requests = [laconic.get_record(request_id, require=True)] + # TODO: assert record type + # all requests + elif discover: + requests = laconic.app_deployment_removal_requests() + + if only_update_state: + if not dry_run: + dump_known_requests(state_file, requests) + return + + previous_requests = load_known_requests(state_file) + requests.sort(key=lambda r: r.createTime) + + # Find deployments. + deployments = {} + for d in laconic.app_deployments(all=True): + deployments[d.id] = d + + # Find removal requests. + removals_by_deployment = {} + removals_by_request = {} + for r in laconic.app_deployment_removals(): + if r.attributes.deployment: + # TODO: should we handle CRNs? + removals_by_deployment[r.attributes.deployment] = r + + requests_to_execute = [] + for r in requests: + if not r.attributes.deployment: + print(f"Skipping removal request {r.id} since it was a cancellation.") + elif r.id in removals_by_request: + print(f"Found satisfied request for {r.id} at {removals_by_request[r.id].id}") + elif r.attributes.deployment in removals_by_deployment: + print( + f"Found removal record for indicated deployment {r.attributes.deployment} at {removals_by_deployment[r.attributes.deployment].id}") + else: + 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.") + + print("Found %d unsatisfied request(s) to process." % len(requests_to_execute)) + + if not dry_run: + for r in requests_to_execute: + try: + process_app_removal_request( + ctx, + laconic, + r, + os.path.abspath(deployment_parent_dir), + delete_volumes, + delete_names + ) + finally: + dump_known_requests(state_file, [r])