From 0a9d68f4e8497f50a3e2b6cb5f4c61551abc9ef8 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Fri, 16 Aug 2024 21:51:15 -0500 Subject: [PATCH 01/13] WIP: Require payment for app deployment requests. --- .../webapp/deploy_webapp_from_registry.py | 57 +++++- .../webapp/undeploy_webapp_from_registry.py | 190 +++++++++++++----- stack_orchestrator/deploy/webapp/util.py | 154 +++++++++++--- 3 files changed, 320 insertions(+), 81 deletions(-) diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index 6948e13b..9b527a6c 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -38,6 +38,7 @@ from stack_orchestrator.deploy.webapp.util import ( generate_hostname_for_app, match_owner, skip_by_tag, + confirm_payment, ) @@ -78,6 +79,9 @@ def process_app_deployment_request( else: fqdn = f"{requested_name}.{default_dns_suffix}" + # Normalize case (just in case) + fqdn = fqdn.lower() + # 3. check ownership of existing dnsrecord vs this request dns_lrn = f"{dns_record_namespace}/{fqdn}" dns_record = laconic.get_record(dns_lrn) @@ -119,7 +123,7 @@ def process_app_deployment_request( app_deployment_lrn = app_deployment_request.attributes.deployment if not app_deployment_lrn.startswith(deployment_record_namespace): raise Exception( - "Deployment CRN %s is not in a supported namespace" + "Deployment LRN %s is not in a supported namespace" % app_deployment_request.attributes.deployment ) @@ -305,6 +309,23 @@ def dump_known_requests(filename, requests, status="SEEN"): @click.option( "--log-dir", help="Output build/deployment logs to directory.", default=None ) +@click.option( + "--min-required-payment", + help="Requests must have a minimum payment to be processed", + default=0, +) +@click.option( + "--payment-address", + help="The address to which payments should be made. " + "Default is the current laconic account.", + default=None, +) +@click.option( + "--all-requests", + help="Handle requests addressed to anyone (by default only requests to" + "my payment address are examined).", + is_flag=True, +) @click.pass_context def command( # noqa: C901 ctx, @@ -326,6 +347,9 @@ def command( # noqa: C901 force_rebuild, recreate_on_deploy, log_dir, + min_required_payment, + payment_address, + all_requests, ): if request_id and discover: print("Cannot specify both --request-id and --discover", file=sys.stderr) @@ -366,6 +390,10 @@ def command( # noqa: C901 exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag] laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr) + if not payment_address: + payment_address = laconic.whoami().address + + main_logger.log(f"Payment address: {payment_address}") # Find deployment requests. # single request @@ -375,18 +403,20 @@ def command( # noqa: C901 # all requests elif discover: main_logger.log("Discovering deployment requests...") - requests = laconic.app_deployment_requests() + if all_requests: + requests = laconic.app_deployment_requests() + else: + requests = laconic.app_deployment_requests({"to": payment_address}) if only_update_state: if not dry_run: dump_known_requests(state_file, requests) return + previous_requests = {} if state_file: main_logger.log(f"Loading known requests from {state_file}...") previous_requests = load_known_requests(state_file) - else: - previous_requests = {} # Collapse related requests. requests.sort(key=lambda r: r.createTime) @@ -466,7 +496,7 @@ def command( # noqa: C901 if r.attributes.request: cancellation_requests[r.attributes.request] = r - requests_to_execute = [] + requests_to_check_for_payment = [] for r in requests_by_name.values(): if r.id in cancellation_requests and match_owner( cancellation_requests[r.id], r @@ -488,7 +518,24 @@ def command( # noqa: C901 ) else: main_logger.log(f"Request {r.id} needs to processed.") + requests_to_check_for_payment.append(r) + + requests_to_execute = [] + if min_required_payment: + for r in requests_to_check_for_payment: + main_logger.log(f"{r.id}: Confirming payment...") + if confirm_payment( + laconic, r, payment_address, min_required_payment, main_logger + ): + main_logger.log(f"{r.id}: Payment confirmed.") requests_to_execute.append(r) + else: + main_logger.log( + f"Skipping request {r.id}: unable to verify payment." + ) + dump_known_requests(state_file, [r], status="UNPAID") + else: + requests_to_execute = requests_to_check_for_payment main_logger.log( "Found %d unsatisfied request(s) to process." % len(requests_to_execute) diff --git a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py index 8585283e..94a53e47 100644 --- a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py @@ -20,18 +20,31 @@ import sys import click -from stack_orchestrator.deploy.webapp.util import LaconicRegistryClient, match_owner, skip_by_tag +from stack_orchestrator.deploy.webapp.util import ( + TimedLogger, + LaconicRegistryClient, + match_owner, + skip_by_tag, +) + +main_logger = TimedLogger(file=sys.stderr) -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) +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) + deployment_dir = os.path.join( + deployment_parent_dir, dns_record.attributes.name.lower() + ) if not os.path.exists(deployment_dir): raise Exception("Deployment directory %s does not exist." % deployment_dir) @@ -41,13 +54,18 @@ def process_app_removal_request(ctx, # Or of the original deployment request. if not matched_owner and deployment_record.attributes.request: - matched_owner = match_owner(app_removal_request, laconic.get_record(deployment_record.attributes.request, require=True)) + matched_owner = match_owner( + app_removal_request, + laconic.get_record(deployment_record.attributes.request, require=True), + ) if matched_owner: - print("Matched deployment ownership:", matched_owner) + main_logger.log("Matched deployment ownership:", matched_owner) else: - raise Exception("Unable to confirm ownership of deployment %s for removal request %s" % - (deployment_record.id, app_removal_request.id)) + raise Exception( + "Unable to confirm ownership of deployment %s for removal request %s" + % (deployment_record.id, app_removal_request.id) + ) # 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 @@ -97,22 +115,65 @@ def dump_known_requests(filename, requests): @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( + "--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.option("--include-tags", help="Only include requests with matching tags (comma-separated).", default="") -@click.option("--exclude-tags", help="Exclude requests with matching tags (comma-separated).", default="") +@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.option( + "--include-tags", + help="Only include requests with matching tags (comma-separated).", + default="", +) +@click.option( + "--exclude-tags", + help="Exclude requests with matching tags (comma-separated).", + default="", +) @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, include_tags, exclude_tags): +def command( + ctx, + laconic_config, + deployment_parent_dir, + request_id, + discover, + state_file, + only_update_state, + delete_names, + delete_volumes, + dry_run, + include_tags, + exclude_tags, +): if request_id and discover: print("Cannot specify both --request-id and --discover", file=sys.stderr) sys.exit(2) @@ -134,10 +195,12 @@ def command(ctx, laconic_config, deployment_parent_dir, # Find deployment removal requests. # single request if request_id: + main_logger.log(f"Retrieving request {request_id}...") requests = [laconic.get_record(request_id, require=True)] # TODO: assert record type # all requests elif discover: + main_logger.log("Discovering removal requests...") requests = laconic.app_deployment_removal_requests() if only_update_state: @@ -145,18 +208,24 @@ def command(ctx, laconic_config, deployment_parent_dir, dump_known_requests(state_file, requests) return - previous_requests = load_known_requests(state_file) + previous_requests = {} + if state_file: + main_logger.log(f"Loading known requests from {state_file}...") + previous_requests = load_known_requests(state_file) + requests.sort(key=lambda r: r.createTime) requests.reverse() # Find deployments. - deployments = {} - for d in laconic.app_deployments(all=True): - deployments[d.id] = d + named_deployments = {} + main_logger.log("Discovering app deployments...") + for d in laconic.app_deployments(all=False): + named_deployments[d.id] = d # Find removal requests. removals_by_deployment = {} removals_by_request = {} + main_logger.log("Discovering deployment removals...") for r in laconic.app_deployment_removals(): if r.attributes.deployment: # TODO: should we handle CRNs? @@ -165,33 +234,50 @@ def command(ctx, laconic_config, deployment_parent_dir, one_per_deployment = {} for r in requests: if not r.attributes.deployment: - print(f"Skipping removal request {r.id} since it was a cancellation.") + main_logger.log( + f"Skipping removal request {r.id} since it was a cancellation." + ) elif r.attributes.deployment in one_per_deployment: - print(f"Skipping removal request {r.id} since it was superseded.") + main_logger.log(f"Skipping removal request {r.id} since it was superseded.") else: one_per_deployment[r.attributes.deployment] = r requests_to_execute = [] for r in one_per_deployment.values(): - if skip_by_tag(r, include_tags, exclude_tags): - print("Skipping removal request %s, filtered by tag (include %s, exclude %s, present %s)" % (r.id, - include_tags, - exclude_tags, - r.attributes.tags)) - 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 " - f"{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) + try: + if r.attributes.deployment not in named_deployments: + main_logger.log( + f"Skipping removal request {r.id} for {r.attributes.deployment} because it does" + f"not appear to refer to a live, named deployment." + ) + elif skip_by_tag(r, include_tags, exclude_tags): + main_logger.log( + "Skipping removal request %s, filtered by tag (include %s, exclude %s, present %s)" + % (r.id, include_tags, exclude_tags, r.attributes.tags) + ) + elif r.id in removals_by_request: + main_logger.log( + f"Found satisfied request for {r.id} at {removals_by_request[r.id].id}" + ) + elif r.attributes.deployment in removals_by_deployment: + main_logger.log( + f"Found removal record for indicated deployment {r.attributes.deployment} at " + f"{removals_by_deployment[r.attributes.deployment].id}" + ) else: - print(f"Skipping unsatisfied request {r.id} because we have seen it before.") + if r.id not in previous_requests: + main_logger.log(f"Request {r.id} needs to processed.") + requests_to_execute.append(r) + else: + main_logger.log( + f"Skipping unsatisfied request {r.id} because we have seen it before." + ) + except Exception as e: + main_logger.log(f"ERROR examining {r.id}: {e}") - print("Found %d unsatisfied request(s) to process." % len(requests_to_execute)) + main_logger.log( + "Found %d unsatisfied request(s) to process." % len(requests_to_execute) + ) if not dry_run: for r in requests_to_execute: @@ -202,7 +288,9 @@ def command(ctx, laconic_config, deployment_parent_dir, r, os.path.abspath(deployment_parent_dir), delete_volumes, - delete_names + delete_names, ) + except Exception as e: + main_logger.log(f"ERROR processing removal request {r.id}: {e}") finally: dump_known_requests(state_file, [r]) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 8adbcb8c..28413bbe 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -22,6 +22,7 @@ import subprocess import sys import tempfile import uuid +from typing import final import yaml @@ -83,6 +84,41 @@ def match_owner(recordA, *records): return None +def is_lrn(name_or_id: str): + if name_or_id: + return str(name_or_id).startswith("lrn://") + return False + + +def is_id(name_or_id: str): + return not is_lrn(name_or_id) + +def confirm_payment(laconic, record, payment_address, min_amount, logger): + if not record.attributes.payment: + logger.log(f"{record.id}: not payment tx") + return False + + tx = laconic.get_tx(record.attributes.payment) + if not tx: + logger.log(f"{record.id}: cannot locate payment tx") + return False + + owner = laconic.get_owner(record) + if tx.from_address != owner: + logger.log(f"{record.id}: {tx.from_address} != {owner}") + return False + + if tx.to_address != payment_address: + logger.log(f"{record.id}: {tx.to_address} != {payment_address}") + return False + + if tx.amount < min_amount: + logger.log(f"{record.id}: {tx.amount} < {min_amount}") + return False + + return True + + class LaconicRegistryClient: def __init__(self, config_file, log_file=None): self.config_file = config_file @@ -90,10 +126,64 @@ class LaconicRegistryClient: self.cache = AttrDict( { "name_or_id": {}, + "accounts": {} } ) - def list_records(self, criteria={}, all=False): + def whoami(self, refresh=False): + if not refresh and "whoami" in self.cache: + return self.cache["whoami"] + + args = ["laconic", "-c", self.config_file, "registry", "account", "get"] + results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + + if len(results): + self.cache["whoami"] = results[0] + return results[0] + + return None + + def get_owner(self, record): + bond = self.get_bond(record.bondId, require=True) + return bond.owner + + def get_account(self, address, refresh=False, require=False): + if not refresh and address in self.cache["accounts"]: + return self.cache["accounts"][address] + + args = ["laconic", "-c", self.config_file, "registry", "account", "get", "--address", address] + results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + if len(results): + self.cache["accounts"][address] = results[0] + return results[0] + + if require: + raise Exception("Cannot locate account:", address) + return None + + def get_bond(self, id, require=False): + if id in self.cache.name_or_id: + return self.cache.name_or_id[id] + + args = ["laconic", "-c", self.config_file, "registry", "bond", "get", "--id", id] + results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + self._add_to_cache(results) + if len(results): + return results[0] + + if require: + raise Exception("Cannot locate bond:", id) + return None + + def list_bonds(self): + args = ["laconic", "-c", self.config_file, "registry", "bond", "list"] + results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + self._add_to_cache(results) + return results + + def list_records(self, criteria=None, all=False): + if criteria is None: + criteria = {} args = ["laconic", "-c", self.config_file, "registry", "record", "list"] if all: @@ -104,22 +194,15 @@ class LaconicRegistryClient: args.append("--%s" % k) args.append(str(v)) - results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args))] + results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] # Most recent records first results.sort(key=lambda r: r.createTime) results.reverse() + self._add_to_cache(results) return results - def is_lrn(self, name_or_id: str): - if name_or_id: - return str(name_or_id).startswith("lrn://") - return False - - def is_id(self, name_or_id: str): - return not self.is_lrn(name_or_id) - def _add_to_cache(self, records): if not records: return @@ -129,9 +212,10 @@ class LaconicRegistryClient: if p.names: for lrn in p.names: self.cache["name_or_id"][lrn] = p - if p.attributes.type not in self.cache: - self.cache[p.attributes.type] = [] - self.cache[p.attributes.type].append(p) + if p.attributes and p.attributes.type: + 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: @@ -142,7 +226,7 @@ class LaconicRegistryClient: args = ["laconic", "-c", self.config_file, "registry", "name", "resolve", name] - parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args))] + parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] if parsed: self._add_to_cache(parsed) return parsed[0] @@ -158,7 +242,7 @@ class LaconicRegistryClient: if name_or_id in self.cache.name_or_id: return self.cache.name_or_id[name_or_id] - if self.is_lrn(name_or_id): + if is_lrn(name_or_id): return self.resolve(name_or_id) args = [ @@ -181,19 +265,37 @@ class LaconicRegistryClient: raise Exception("Cannot locate record:", name_or_id) return None - def app_deployment_requests(self, all=True): - return self.list_records({"type": "ApplicationDeploymentRequest"}, all) + def app_deployment_requests(self, criteria=None, all=True): + if criteria is None: + criteria = {} + criteria = criteria.copy() + criteria["type"] = "ApplicationDeploymentRequest" + return self.list_records(criteria, all) - def app_deployments(self, all=True): - return self.list_records({"type": "ApplicationDeploymentRecord"}, all) + def app_deployments(self, criteria=None, all=True): + if criteria is None: + criteria = {} + criteria = criteria.copy() + criteria["type"] = "ApplicationDeploymentRecord" + return self.list_records(criteria, all) - def app_deployment_removal_requests(self, all=True): - return self.list_records({"type": "ApplicationDeploymentRemovalRequest"}, all) + def app_deployment_removal_requests(self, criteria=None, all=True): + if criteria is None: + criteria = {} + criteria = criteria.copy() + criteria["type"] = "ApplicationDeploymentRemovalRequest" + return self.list_records(criteria, all) - def app_deployment_removals(self, all=True): - return self.list_records({"type": "ApplicationDeploymentRemovalRecord"}, all) + def app_deployment_removals(self, criteria=None, all=True): + if criteria is None: + criteria = {} + criteria = criteria.copy() + criteria["type"] = "ApplicationDeploymentRemovalRecord" + return self.list_records(criteria, all) - def publish(self, record, names=[]): + def publish(self, record, names=None): + if names is None: + names = [] tmpdir = tempfile.mkdtemp() try: record_fname = os.path.join(tmpdir, "record.yml") @@ -248,7 +350,9 @@ def determine_base_container(clone_dir, app_type="webapp"): return base_container -def build_container_image(app_record, tag, extra_build_args=[], logger=None): +def build_container_image(app_record, tag, extra_build_args=None, logger=None): + if extra_build_args is None: + extra_build_args = [] tmpdir = tempfile.mkdtemp() # TODO: determine if this code could be calling into the Python git library like setup-repositories -- 2.45.2 From 07030044ec0f3a13f494e5e73401d94c7aee40aa Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Fri, 16 Aug 2024 21:59:52 -0500 Subject: [PATCH 02/13] get_tx() --- stack_orchestrator/deploy/webapp/util.py | 207 ++++++++++++++++++----- 1 file changed, 161 insertions(+), 46 deletions(-) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 28413bbe..997c15b8 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -93,6 +93,7 @@ def is_lrn(name_or_id: str): def is_id(name_or_id: str): return not is_lrn(name_or_id) + def confirm_payment(laconic, record, payment_address, min_amount, logger): if not record.attributes.payment: logger.log(f"{record.id}: not payment tx") @@ -126,7 +127,8 @@ class LaconicRegistryClient: self.cache = AttrDict( { "name_or_id": {}, - "accounts": {} + "accounts": {}, + "txs": {}, } ) @@ -135,7 +137,9 @@ class LaconicRegistryClient: return self.cache["whoami"] args = ["laconic", "-c", self.config_file, "registry", "account", "get"] - results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + results = [ + AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r + ] if len(results): self.cache["whoami"] = results[0] @@ -151,8 +155,19 @@ class LaconicRegistryClient: if not refresh and address in self.cache["accounts"]: return self.cache["accounts"][address] - args = ["laconic", "-c", self.config_file, "registry", "account", "get", "--address", address] - results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + args = [ + "laconic", + "-c", + self.config_file, + "registry", + "account", + "get", + "--address", + address, + ] + results = [ + AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r + ] if len(results): self.cache["accounts"][address] = results[0] return results[0] @@ -165,8 +180,19 @@ class LaconicRegistryClient: if id in self.cache.name_or_id: return self.cache.name_or_id[id] - args = ["laconic", "-c", self.config_file, "registry", "bond", "get", "--id", id] - results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + args = [ + "laconic", + "-c", + self.config_file, + "registry", + "bond", + "get", + "--id", + id, + ] + results = [ + AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r + ] self._add_to_cache(results) if len(results): return results[0] @@ -177,7 +203,9 @@ class LaconicRegistryClient: def list_bonds(self): args = ["laconic", "-c", self.config_file, "registry", "bond", "list"] - results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + results = [ + AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r + ] self._add_to_cache(results) return results @@ -194,7 +222,9 @@ class LaconicRegistryClient: args.append("--%s" % k) args.append(str(v)) - results = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + results = [ + AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r + ] # Most recent records first results.sort(key=lambda r: r.createTime) @@ -226,7 +256,9 @@ class LaconicRegistryClient: args = ["laconic", "-c", self.config_file, "registry", "name", "resolve", name] - parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + parsed = [ + AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r + ] if parsed: self._add_to_cache(parsed) return parsed[0] @@ -256,7 +288,9 @@ class LaconicRegistryClient: name_or_id, ] - parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r] + parsed = [ + AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r + ] if len(parsed): self._add_to_cache(parsed) return parsed[0] @@ -265,6 +299,31 @@ class LaconicRegistryClient: raise Exception("Cannot locate record:", name_or_id) return None + def get_tx(self, txHash, require=False): + if txHash in self.cache["txs"]: + return self.cache["txs"][txHash] + + args = [ + "laconic", + "-c", + self.config_file, + "registry", + "tx", + "get", + "--hash", + txHash, + ] + + parsed = [ + AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r + ] + if len(parsed): + self.cache["txs"][txHash] = parsed[0] + return parsed[0] + + if require: + raise Exception("Cannot locate tx:", hash) + def app_deployment_requests(self, criteria=None, all=True): if criteria is None: criteria = {} @@ -299,22 +358,23 @@ class LaconicRegistryClient: tmpdir = tempfile.mkdtemp() try: record_fname = os.path.join(tmpdir, "record.yml") - record_file = open(record_fname, 'w') + record_file = open(record_fname, "w") yaml.dump(record, record_file) record_file.close() - print(open(record_fname, 'r').read(), file=self.log_file) + print(open(record_fname, "r").read(), file=self.log_file) new_record_id = json.loads( logged_cmd( self.log_file, - "laconic", "-c", + "laconic", + "-c", self.config_file, "registry", "record", "publish", "--filename", - record_fname - ) + record_fname, + ) )["id"] for name in names: self.set_name(name, new_record_id) @@ -323,10 +383,29 @@ class LaconicRegistryClient: logged_cmd(self.log_file, "rm", "-rf", tmpdir) def set_name(self, name, record_id): - logged_cmd(self.log_file, "laconic", "-c", self.config_file, "registry", "name", "set", name, record_id) + logged_cmd( + self.log_file, + "laconic", + "-c", + self.config_file, + "registry", + "name", + "set", + name, + record_id, + ) def delete_name(self, name): - logged_cmd(self.log_file, "laconic", "-c", self.config_file, "registry", "name", "delete", name) + logged_cmd( + self.log_file, + "laconic", + "-c", + self.config_file, + "registry", + "name", + "delete", + name, + ) def file_hash(filename): @@ -369,9 +448,15 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None): if github_token: logger.log("Github token detected, setting it in the git environment") git_config_args = [ - "git", "config", "--global", f"url.https://{github_token}:@github.com/.insteadOf", "https://github.com/" - ] - result = subprocess.run(git_config_args, stdout=logger.file, stderr=logger.file) + "git", + "config", + "--global", + f"url.https://{github_token}:@github.com/.insteadOf", + "https://github.com/", + ] + result = subprocess.run( + git_config_args, stdout=logger.file, stderr=logger.file + ) result.check_returncode() if ref: # TODO: Determing branch or hash, and use depth 1 if we can. @@ -379,30 +464,50 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None): # Never prompt git_env["GIT_TERMINAL_PROMPT"] = "0" try: - subprocess.check_call(["git", "clone", repo, clone_dir], env=git_env, stdout=logger.file, stderr=logger.file) + subprocess.check_call( + ["git", "clone", repo, clone_dir], + env=git_env, + stdout=logger.file, + stderr=logger.file, + ) except Exception as e: logger.log(f"git clone failed. Is the repository {repo} private?") raise e try: - subprocess.check_call(["git", "checkout", ref], cwd=clone_dir, env=git_env, stdout=logger.file, stderr=logger.file) + subprocess.check_call( + ["git", "checkout", ref], + cwd=clone_dir, + env=git_env, + stdout=logger.file, + stderr=logger.file, + ) except Exception as e: logger.log(f"git checkout failed. Does ref {ref} exist?") raise e else: # TODO: why is this code different vs the branch above (run vs check_call, and no prompt disable)? - result = subprocess.run(["git", "clone", "--depth", "1", repo, clone_dir], stdout=logger.file, stderr=logger.file) + result = subprocess.run( + ["git", "clone", "--depth", "1", repo, clone_dir], + stdout=logger.file, + stderr=logger.file, + ) result.check_returncode() - base_container = determine_base_container(clone_dir, app_record.attributes.app_type) + base_container = determine_base_container( + clone_dir, app_record.attributes.app_type + ) logger.log("Building webapp ...") build_command = [ sys.argv[0], "--verbose", "build-webapp", - "--source-repo", clone_dir, - "--tag", tag, - "--base-container", base_container + "--source-repo", + clone_dir, + "--tag", + tag, + "--base-container", + base_container, ] if extra_build_args: build_command.append("--extra-build-args") @@ -416,8 +521,11 @@ def build_container_image(app_record, tag, extra_build_args=None, logger=None): def push_container_image(deployment_dir, logger): logger.log("Pushing images ...") - result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, "push-images"], - stdout=logger.file, stderr=logger.file) + result = subprocess.run( + [sys.argv[0], "deployment", "--dir", deployment_dir, "push-images"], + stdout=logger.file, + stderr=logger.file, + ) result.check_returncode() logger.log("Finished pushing images.") @@ -435,27 +543,34 @@ def deploy_to_k8s(deploy_record, deployment_dir, recreate, logger): for command in commands_to_run: logger.log(f"Running {command} command on deployment dir: {deployment_dir}") - result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, command], - stdout=logger.file, stderr=logger.file) + result = subprocess.run( + [sys.argv[0], "deployment", "--dir", deployment_dir, command], + stdout=logger.file, + stderr=logger.file, + ) result.check_returncode() logger.log(f"Finished {command} command on deployment dir: {deployment_dir}") logger.log("Finished deploying to k8s.") -def publish_deployment(laconic: LaconicRegistryClient, - app_record, - deploy_record, - deployment_lrn, - dns_record, - dns_lrn, - deployment_dir, - app_deployment_request=None, - logger=None): +def publish_deployment( + laconic: LaconicRegistryClient, + app_record, + deploy_record, + deployment_lrn, + dns_record, + dns_lrn, + deployment_dir, + app_deployment_request=None, + logger=None, +): if not deploy_record: deploy_ver = "0.0.1" else: - deploy_ver = "0.0.%d" % (int(deploy_record.attributes.version.split(".")[-1]) + 1) + deploy_ver = "0.0.%d" % ( + int(deploy_record.attributes.version.split(".")[-1]) + 1 + ) if not dns_record: dns_ver = "0.0.1" @@ -473,9 +588,7 @@ def publish_deployment(laconic: LaconicRegistryClient, "version": dns_ver, "name": fqdn, "resource_type": "A", - "meta": { - "so": uniq.hex - }, + "meta": {"so": uniq.hex}, } } if app_deployment_request: @@ -495,7 +608,7 @@ def publish_deployment(laconic: LaconicRegistryClient, "dns": dns_id, "meta": { "config": file_hash(os.path.join(deployment_dir, "config.env")), - "so": uniq.hex + "so": uniq.hex, }, } } @@ -511,7 +624,9 @@ def publish_deployment(laconic: LaconicRegistryClient, 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) + app = laconic.get_record( + app_deployment_request.attributes.application, require=True + ) dns_name = generate_hostname_for_app(app) elif dns_name.startswith("lrn://"): record = laconic.get_record(dns_name, require=True) -- 2.45.2 From b449d88b6c66fda073533a04269798ede2ae0514 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Fri, 16 Aug 2024 22:33:23 -0500 Subject: [PATCH 03/13] Include payment info in deployment record. --- .../webapp/deploy_webapp_from_registry.py | 19 +++++++++++++++++-- stack_orchestrator/deploy/webapp/util.py | 19 +++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index 9b527a6c..e4053336 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -55,6 +55,7 @@ def process_app_deployment_request( force_rebuild, fqdn_policy, recreate_on_deploy, + payment_address, logger, ): logger.log("BEGIN - process_app_deployment_request") @@ -226,6 +227,7 @@ def process_app_deployment_request( dns_lrn, deployment_dir, app_deployment_request, + payment_address, logger, ) logger.log("Publication complete.") @@ -482,11 +484,17 @@ def command( # noqa: C901 # Find deployments. main_logger.log("Discovering existing app deployments...") - deployments = laconic.app_deployments() + if all_requests: + deployments = laconic.app_deployments() + else: + deployments = laconic.app_deployments({"by": payment_address}) deployments_by_request = {} + deployments_by_payment = {} for d in deployments: if d.attributes.request: deployments_by_request[d.attributes.request] = d + if d.attributes.payment: + deployments_by_request[d.attributes.payment] = d # Find removal requests. main_logger.log("Discovering deployment removal and cancellation requests...") @@ -524,7 +532,13 @@ def command( # noqa: C901 if min_required_payment: for r in requests_to_check_for_payment: main_logger.log(f"{r.id}: Confirming payment...") - if confirm_payment( + if r.attributes.payment in deployments_by_payment: + main_logger.log( + f"Skipping request {r.id}: payment already applied to deployment " + f"{deployments_by_payment[r.attributes.payment].id}" + ) + dump_known_requests(state_file, [r], status="UNPAID") + elif confirm_payment( laconic, r, payment_address, min_required_payment, main_logger ): main_logger.log(f"{r.id}: Payment confirmed.") @@ -578,6 +592,7 @@ def command( # noqa: C901 force_rebuild, fqdn_policy, recreate_on_deploy, + payment_address, build_logger, ) status = "DEPLOYED" diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 997c15b8..ec9d20d3 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -96,7 +96,7 @@ def is_id(name_or_id: str): def confirm_payment(laconic, record, payment_address, min_amount, logger): if not record.attributes.payment: - logger.log(f"{record.id}: not payment tx") + logger.log(f"{record.id}: no payment tx info") return False tx = laconic.get_tx(record.attributes.payment) @@ -147,9 +147,12 @@ class LaconicRegistryClient: return None - def get_owner(self, record): - bond = self.get_bond(record.bondId, require=True) - return bond.owner + def get_owner(self, record, require=False): + bond = self.get_bond(record.bondId, require) + if bond: + return bond.owner + + return bond def get_account(self, address, refresh=False, require=False): if not refresh and address in self.cache["accounts"]: @@ -563,6 +566,7 @@ def publish_deployment( dns_lrn, deployment_dir, app_deployment_request=None, + payment_address=None, logger=None, ): if not deploy_record: @@ -614,6 +618,13 @@ def publish_deployment( } if app_deployment_request: new_deployment_record["record"]["request"] = app_deployment_request.id + if app_deployment_request.attributes.payment: + new_deployment_record["record"][ + "payment" + ] = app_deployment_request.attributes.payment + + if payment_address: + new_deployment_record["record"]["by"] = payment_address if logger: logger.log("Publishing ApplicationDeploymentRecord.") -- 2.45.2 From 2f1cde16b77c081f520a286e060a9726ee612602 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Mon, 19 Aug 2024 19:35:44 -0500 Subject: [PATCH 04/13] Update in re https://git.vdb.to/cerc-io/laconic-registry-cli/pulls/78 --- stack_orchestrator/deploy/webapp/util.py | 36 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index ec9d20d3..a98b254a 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -104,17 +104,37 @@ def confirm_payment(laconic, record, payment_address, min_amount, logger): logger.log(f"{record.id}: cannot locate payment tx") return False + if tx.code != 0: + logger.log( + f"{record.id}: payment tx {tx.hash} was not successful - code: {tx.code}, log: {tx.log}" + ) + return False + owner = laconic.get_owner(record) - if tx.from_address != owner: - logger.log(f"{record.id}: {tx.from_address} != {owner}") + if tx.sender != owner: + logger.log( + f"{record.id}: payment sender {tx.sender} in tx {tx.hash} does not match deployment request {owner}" + ) return False - if tx.to_address != payment_address: - logger.log(f"{record.id}: {tx.to_address} != {payment_address}") + if tx.recipient != payment_address: + logger.log( + f"{record.id}: payment recipient {tx.recipient} in tx {tx.hash} does not match {payment_address}" + ) return False - if tx.amount < min_amount: - logger.log(f"{record.id}: {tx.amount} < {min_amount}") + pay_denom = "".join([i for i in tx.amount if not i.isdigit()]) + if pay_denom != "alnt": + logger.log( + f"{record.id}: {pay_denom} in tx {tx.hash} is not an expected payment denomination" + ) + return False + + pay_amount = int("".join([i for i in tx.amount if i.isdigit()])) + if pay_amount < min_amount: + logger.log( + f"{record.id}: payment amount {tx.amount} is less than minimum {min_amount}" + ) return False return True @@ -311,8 +331,8 @@ class LaconicRegistryClient: "-c", self.config_file, "registry", - "tx", - "get", + "tokens", + "gettx", "--hash", txHash, ] -- 2.45.2 From 09c9214e4c44b97d8966e6ce5b09331f132dcd98 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Mon, 19 Aug 2024 19:39:43 -0500 Subject: [PATCH 05/13] log --- stack_orchestrator/deploy/webapp/util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index a98b254a..f8b4ad01 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -110,10 +110,11 @@ def confirm_payment(laconic, record, payment_address, min_amount, logger): ) return False - owner = laconic.get_owner(record) - if tx.sender != owner: + req_owner = laconic.get_owner(record) + if tx.sender != req_owner: logger.log( - f"{record.id}: payment sender {tx.sender} in tx {tx.hash} does not match deployment request {owner}" + f"{record.id}: payment sender {tx.sender} in tx {tx.hash} does not match deployment " + f"request owner {req_owner}" ) return False -- 2.45.2 From 023a6402529402f2f09c87a86bf5275d8ca28f52 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Tue, 20 Aug 2024 11:41:34 -0500 Subject: [PATCH 06/13] lint --- .../deploy/webapp/undeploy_webapp_from_registry.py | 2 +- stack_orchestrator/deploy/webapp/util.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py index 94a53e47..3580e5ca 100644 --- a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py @@ -160,7 +160,7 @@ def dump_known_requests(filename, requests): default="", ) @click.pass_context -def command( +def command( # noqa: C901 ctx, laconic_config, deployment_parent_dir, diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index f8b4ad01..67cfb807 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -22,8 +22,6 @@ import subprocess import sys import tempfile import uuid -from typing import final - import yaml -- 2.45.2 From 56b010f512bc1fe855c4ca04e0a0383e822378e3 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Tue, 20 Aug 2024 14:58:06 -0500 Subject: [PATCH 07/13] Not a list --- stack_orchestrator/deploy/webapp/util.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 67cfb807..18e0566a 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -336,12 +336,15 @@ class LaconicRegistryClient: txHash, ] - parsed = [ - AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args)) if r - ] - if len(parsed): - self.cache["txs"][txHash] = parsed[0] - return parsed[0] + parsed = None + try: + parsed = AttrDict(json.loads(logged_cmd(self.log_file, *args))) + except: + pass + + if parsed: + self.cache["txs"][txHash] = parsed + return parsed if require: raise Exception("Cannot locate tx:", hash) -- 2.45.2 From 916724f1e2517a963aebc2b2abc05a805a2ce24b Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Tue, 20 Aug 2024 16:57:42 -0500 Subject: [PATCH 08/13] Undeploy --- .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 176 bytes .../__pycache__/deploy_webapp.cpython-310.pyc | Bin 0 -> 3240 bytes ...eploy_webapp_from_registry.cpython-310.pyc | Bin 0 -> 12782 bytes .../__pycache__/run_webapp.cpython-310.pyc | Bin 0 -> 1452 bytes ...eploy_webapp_from_registry.cpython-310.pyc | Bin 0 -> 6295 bytes .../webapp/__pycache__/util.cpython-310.pyc | Bin 0 -> 15737 bytes .../webapp/deploy_webapp_from_registry.py | 11 +---- .../webapp/undeploy_webapp_from_registry.py | 45 ++++++++++++++++-- stack_orchestrator/deploy/webapp/util.py | 23 ++++++++- 9 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 stack_orchestrator/deploy/webapp/__pycache__/__init__.cpython-310.pyc create mode 100644 stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp.cpython-310.pyc create mode 100644 stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp_from_registry.cpython-310.pyc create mode 100644 stack_orchestrator/deploy/webapp/__pycache__/run_webapp.cpython-310.pyc create mode 100644 stack_orchestrator/deploy/webapp/__pycache__/undeploy_webapp_from_registry.cpython-310.pyc create mode 100644 stack_orchestrator/deploy/webapp/__pycache__/util.cpython-310.pyc diff --git a/stack_orchestrator/deploy/webapp/__pycache__/__init__.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a3247a5410592fadbfb660c30a3c66407bb144d2 GIT binary patch literal 176 zcmd1j<>g`kf`rDcX(0MBh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o10aKO;XkRlg)P zCowrYwNgJhwJ2G?xCF@2%`ZyMNG&cYN-W7Qf(XT<2&JSJ16fwg1SD7(0N&#*U;qFB literal 0 HcmV?d00001 diff --git a/stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4f90fda0497a83b856e79d0a64becbc70b91a8e GIT binary patch literal 3240 zcmai0PjBPK73c83C|Q!V-nDnVY1qv+3pJpX21UDdgP_0$IrLxz*Z^&HQEAS|qD_kI zaJ04v>QHD;1s3@LZ2-Hset>?IUgp|UKS6=CNc!H8w&iV$kl_4zGxKKVz2EyiW?NgH z2G4JQ|JUi?2AcMF>})za%S^z}qfjj=&cW+Y~6jV;wS6FYUr zj%r(po3_TS)Ej%MZzt`vGw!5Y<1N*9l5W}?_f*?W`swy~8`>>4pdK4cP1>fN*XH;h z-2zEhNqV#ok{!BD2d}m9eWqPLxYC$*x=ZiTo!8o$tjTzf?OpCE=|g&-KA^jAbeQpw z?$L)g?MHN1e?42apahOc9T*TSD z_y~r(@jMnFU_3;b>_?o1g563$@%=_dS)OFeNtDe6yAUw2^Qn@&$kH^N(-*Z|cI$a5 zn~MpJxwPUmJYkWJXKx+{Y8Sp2@D+c6LTD8jcBNPPC831sSNh5jdS$H1mxGmAnU}_u zwz4Yg(xgU3VAT2*fWm}#4e*$|Khlc#&I!@*{SAD$mYQq*%kIjq>`PdA1*4m5sPgsc+m8Nb9Nl&c_6^X30Uc1nI`3a#CSAnM$k54JZJnc z$)Yfkp@b7Z3z1osvpFC4U4;fLiB0Ea!w8AThtP04s*V zk0}5vfSJC+iUpVf?^nhy)SJFnco}Va9@kMz;l-((OIY=P@bb&eRSGZmQR#cWKNBJk za-Ll*525G#fj`R%5#T)!eKiB6JX;7>)HCW?=+#62+1F%LK7FQEqTKhDsz79ier@%` z=O3z3-UYJ&P7E}ti+dd!ry^HdWtLP1ne$*c;boLW`cg% z^o~;Y0GI7zv4;gBsxBTuOZFNBfGV~883+s9$<=jY>gG3H)BU&Wc*G^%EexK*gb1Vm z`az^)8c()`(`&4lrvYQX==9fJ`OfZ(0K5%RyiqdJgpfr&mJ{ua+&N+j4k#@0|s+zKd z30C%+nb&+GjkD*)aH}Q^yH>|7FbX%JkS2PXx3F(xfp+9*QI0WFhnSuf(&jA9Ua?_E z8j-lTWt18N?X$%(!@!-!CqVzm%@fW};OKlQT{eHE0+jE7N~w#fLXWf*MaTyY5daAx z<6ozOx3RH(Ct;weeg@i#kD+iKSMQUqZUaVieTSIe>V{6 zXuK(m73fFL?tmHiPoY_E1p>5)Qhj{Nd+3dlgeWaxL+Z-u-FDK>}FWxB;YNbH<58zYH;n;^5=K7)cMI*bNuT$K7_GSD`s(_G)Kvz5R9X`TQdcK_;UC{{UU7atxi?= z@f^$q%V83WAg?UH80!2vTuZu~{cZtLS~|_?%3gf{ku`csD+u5aWd~0J(t`0IXqANqizrA)K&y(^eHB7;jw>NBz0PY7$ zjHk-~D(RyGYSV#V5s1LS%Dp!UTZI=tfFVA@;zKOna=u+}nZr6c0%M=~S_rGh}L@aYwaAbOE0MjV4s<{hoDXkO8jJfROVNu+$3Pyyq>z`F+ zrVVQk+4sY3<@;5+ru9aeEYyPSPzUr+a1_}32oa&NmsoPc;qqp4|-o< AumAu6 literal 0 HcmV?d00001 diff --git a/stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp_from_registry.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp_from_registry.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbd1a31d2bc8464a75bd13c96a309cd5ea7b6704 GIT binary patch literal 12782 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 literal 0 HcmV?d00001 diff --git a/stack_orchestrator/deploy/webapp/__pycache__/run_webapp.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/run_webapp.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ea5bdf4c03e53f1b0bb1a6c17a27008e9131ad2 GIT binary patch literal 1452 zcmZWp&2HQ_5Ek{nTCMFmgd&K>_>N2k1NW8F=j}uMiYPhmsdaPzszsa)vYWkz%8fr{Q@1>!0)aK-2zK z%G<+1YY5IJ2&!>?$W*AvKp});v!FlOKYyq|4>QZn+ZN0TkzOO#& zS~2|j`)|LQPNV5hPmZO5jg@;Ph?k6?Ml}eLm5Z9+Qr=d@RUvvt!;v%+aW1XyqDyz% z3u&tQ_`ceGxGJ+3D~OVORSVz|g*xF`B^bxIfEi=c`W5N+*#Pg4QLn6rPb9|?WMYy7@*k^xuz&yn literal 0 HcmV?d00001 diff --git a/stack_orchestrator/deploy/webapp/__pycache__/undeploy_webapp_from_registry.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/undeploy_webapp_from_registry.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49942936420c49ef5aec07d231162171797c532f GIT binary patch literal 6295 zcmb7I&2t;cb)TLYd=Ladkl=@?&yjYuE7$_0IR02GifY%A)~;kNXDOvNVI_qcqMP7A z0}R+bAjPR+N)@VdQ)-jSExT3V!G}HO4@lzjAK-F1m(*OHi;s?PTd|qn>luK+F4vU+ zH9ga>U%!6+y8HF-yi_<}JLbGX1Nf@-7~Aj~NCxIP;CI^v$-IuA5t? zep_3Xe%o8Ne&@Dwc$@d z3%tOKuZ*ntnJ@6lYfIM_d3CC`#Fu&PwW(WH_$pe~`1)&G z*DvsmN!vw!32m1%8CUpK)UIVU2jAEEjjZ+te{+iSCVz{+J>k5?>yz3t{~G`LE901b z)qktX*;Au_`&VSuCv`iSf9CZB{~+jegiKaHaN9xOYj28{r2?>JRa#Cs?qUb&c5UvaZP)H(#4a zc}>ZKVr7&3z)FaNx zPtdY1|DMW6#$OkX7SMVjUZB>!zr%dj!GZEiZU2O^e;jU%F4FullJQhO^~ER8umA-q z_J*Dm(c6#2zz+_4STTo#&<+E6=-gBe4}^01LFkAVSk-29<%hJKf;;@IgI-&;>kaNj z5C6RHZu`Os0|%R7$CEv0(&_{|)4d>kCy6=F<@)i#pS*#J?l(STBk z%^@qQq9S||ipGB64|_r-dFUHRp4@nEFz~&$8+t+iY(AUm0Gpa8%Kbs}Zu2hot{ol@ zu=n~7%o6T9#B zM3VD^18nrXHkV4~9=~Xd0VyPH2$oAUH5>`A-}2LAW8s~c9#1TF2!XEb>?e75Fc5v7 zl+Vzt8;vPeqQFa`u^+=Mv5nc^8TQk9E3LP7WYB}wsrzaU4wFln z@muHE>N-?WTPW;ek>yX9tvo9;`?PGAS=lOECNo);*}t=M6=s?yD>3ubK3yr=_$sj? zGc||J@c$qCv{basJm_VXH>tGE3S`N5AihnfVf>@-93Jfz-^533$ZBYG*~pBI6NYbV zWQVyJm)o&<0%z^vyw}Yq36u4_Ttx@DM#V)G*tQBA-}VESCw71hS7%9)JTmPaZR)i6 z4l&n=t@XT5it@o#@7&$zb2( z!f{jo*Kx?8+R|NmxQr+Ned9+SGZG1`^>B%FP7k%@gp60aQ9J^-^j(J=3 zVTD~M-1Xq9$9_91HyYWr8y=6o@oZN(@BLoan7Up#?QFx>nS4wWERo|??Eqdfdi*H+ za&S<`jE?L34*4pt-}%3cq28}sT69wIRBOj~J4u0y9T#Vww8klhUni4AfA~Hb4k{^} zWq}ecj=LQULyRrp>rNz_gjGdr9}qYCWW?=nvC%S8sm-07@5bN9wj41wbThan5pYzL}*F^(#4yD6nWUd zy%Bxmk)9?GCX>4DbceWYr5FSsPcbHJ%b zxH zWqCArVgTahr35e?grztaFC1rn&@+xI0EEYye=*LT$pvuDVbn^z&@CgTSbAmf<<|rs z7e)&NGtqNV_o}6%bSoSY#%s(2fU+7_)=_0mKW05@BK9G6R+$W^1o&{HqJQWm5Jn^ zc8sIdcx6=6g*KmD;_d`uz-Z#Mcj^?~V6iB-oFVokCLA6h9&3#YzUXGVE|EA`@meO>O%sMlD z9Fh`OwC20Iho*r4dbT@O0sHj|A;6%$bAx)I{f)H!BV+H^wh^v>%ntvN8Aof(h|+a> zGvm7%uZ=bQ$oNZxgXeG7_I{nd57>dBrM?wjh*ytUI`4SyW2Sx?uVVEc!oKfBi{VCW z#O1gaUhG~PU5)`J;vzz&JTpEr`q#4Ye=#%u+rMx8J|bw+gU0{J&^x-;0Z3u6^xnyI z{Qw$gLCUQs(Vd5GpTZ9{5N&Vg(Af^)K&P)aeH)!!QnGpe6+d1&v1dsTt)>~fmHWHc55ou_zyLpZeqa05`oVkQA_M#hQp59XfvG{qSR zx^e)h-rzGcF`IW23m)@}D5P{Bh(3f99X$9q5=tXnFrrHtNN(QugSP9d2lyECea>!w zrBd=w(LWvI1NwS0scQ5?@J@b+;@|0G6Gc?p6i^YDEIfJ!B%H`s=kea4I6kr+V2G%h@?>@b2Ze8XP98uaV)Gd_ov65( z#r@HW(-(r{Zl0h}vd5LlG=}{z4;(oYI~@pT*WCxWBSZ+hZwqJz7*l<`x%s0_%*pL} zeOj!@yyrw;Pu0B1opZ$AQuQe*764fmPe>&*Svlps_| zIopZ4rqdSkd&IspjsPa1XtJbFz?3aj!g#VaKChH>JhO9Bo2F-~Yd4cD1N*jc9KXP= z10>Z7gO+$9+Q`NhXA@e>%sHlF9qIkbTu|8t?ak*lxpl;jCY?;{>o&y$;2USz4JGcV25_{}ua(xj}EB&VI83c5b^hLnFn zU;2|yg0b`baSA1N|=jUveC!^?;jLGcZmUCyuKzb&R<(X==bE%o^-`csf-)!&c_P5yo E0$M&Dp#T5? literal 0 HcmV?d00001 diff --git a/stack_orchestrator/deploy/webapp/__pycache__/util.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/util.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cca3e338f8b170a1bcc35e8c106882e2d683161 GIT binary patch literal 15737 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 LeeV^okk9{rW^+TN literal 0 HcmV?d00001 diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index e4053336..f8dd796f 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -489,12 +489,9 @@ def command( # noqa: C901 else: deployments = laconic.app_deployments({"by": payment_address}) deployments_by_request = {} - deployments_by_payment = {} for d in deployments: if d.attributes.request: deployments_by_request[d.attributes.request] = d - if d.attributes.payment: - deployments_by_request[d.attributes.payment] = d # Find removal requests. main_logger.log("Discovering deployment removal and cancellation requests...") @@ -532,13 +529,7 @@ def command( # noqa: C901 if min_required_payment: for r in requests_to_check_for_payment: main_logger.log(f"{r.id}: Confirming payment...") - if r.attributes.payment in deployments_by_payment: - main_logger.log( - f"Skipping request {r.id}: payment already applied to deployment " - f"{deployments_by_payment[r.attributes.payment].id}" - ) - dump_known_requests(state_file, [r], status="UNPAID") - elif confirm_payment( + if confirm_payment( laconic, r, payment_address, min_required_payment, main_logger ): main_logger.log(f"{r.id}: Payment confirmed.") diff --git a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py index 3580e5ca..631a2fef 100644 --- a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py @@ -25,6 +25,7 @@ from stack_orchestrator.deploy.webapp.util import ( LaconicRegistryClient, match_owner, skip_by_tag, + confirm_payment, ) main_logger = TimedLogger(file=sys.stderr) @@ -159,6 +160,23 @@ def dump_known_requests(filename, requests): help="Exclude requests with matching tags (comma-separated).", default="", ) +@click.option( + "--min-required-payment", + help="Requests must have a minimum payment to be processed", + default=0, +) +@click.option( + "--payment-address", + help="The address to which payments should be made. " + "Default is the current laconic account.", + default=None, +) +@click.option( + "--all-requests", + help="Handle requests addressed to anyone (by default only requests to" + "my payment address are examined).", + is_flag=True, +) @click.pass_context def command( # noqa: C901 ctx, @@ -173,6 +191,9 @@ def command( # noqa: C901 dry_run, include_tags, exclude_tags, + min_required_payment, + payment_address, + all_requests, ): if request_id and discover: print("Cannot specify both --request-id and --discover", file=sys.stderr) @@ -201,7 +222,10 @@ def command( # noqa: C901 # all requests elif discover: main_logger.log("Discovering removal requests...") - requests = laconic.app_deployment_removal_requests() + if all_requests: + requests = laconic.app_deployment_removal_requests() + else: + requests = laconic.app_deployment_removal_requests({"to": payment_address}) if only_update_state: if not dry_run: @@ -242,7 +266,7 @@ def command( # noqa: C901 else: one_per_deployment[r.attributes.deployment] = r - requests_to_execute = [] + requests_to_check_for_payment = [] for r in one_per_deployment.values(): try: if r.attributes.deployment not in named_deployments: @@ -267,7 +291,7 @@ def command( # noqa: C901 else: if r.id not in previous_requests: main_logger.log(f"Request {r.id} needs to processed.") - requests_to_execute.append(r) + requests_to_check_for_payment.append(r) else: main_logger.log( f"Skipping unsatisfied request {r.id} because we have seen it before." @@ -275,6 +299,21 @@ def command( # noqa: C901 except Exception as e: main_logger.log(f"ERROR examining {r.id}: {e}") + requests_to_execute = [] + if min_required_payment: + for r in requests_to_check_for_payment: + main_logger.log(f"{r.id}: Confirming payment...") + if confirm_payment( + laconic, r, payment_address, min_required_payment, main_logger + ): + main_logger.log(f"{r.id}: Payment confirmed.") + requests_to_execute.append(r) + else: + main_logger.log(f"Skipping request {r.id}: unable to verify payment.") + dump_known_requests(state_file, [r]) + else: + requests_to_execute = requests_to_check_for_payment + main_logger.log( "Found %d unsatisfied request(s) to process." % len(requests_to_execute) ) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 18e0566a..0d8c3317 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -93,6 +93,11 @@ def is_id(name_or_id: str): def confirm_payment(laconic, record, payment_address, min_amount, logger): + req_owner = laconic.get_owner(record) + if req_owner == payment_address: + # No need to confirm payment if the sender and recipient are the same account. + return True + if not record.attributes.payment: logger.log(f"{record.id}: no payment tx info") return False @@ -108,7 +113,6 @@ def confirm_payment(laconic, record, payment_address, min_amount, logger): ) return False - req_owner = laconic.get_owner(record) if tx.sender != req_owner: logger.log( f"{record.id}: payment sender {tx.sender} in tx {tx.hash} does not match deployment " @@ -136,6 +140,23 @@ def confirm_payment(laconic, record, payment_address, min_amount, logger): ) return False + # Check if the payment was already used on a + used = laconic.app_deployments( + {"by": payment_address, "payment": tx.hash}, all=True + ) + if len(used): + logger.log(f"{record.id}: payment {tx.hash} already used on deployment {used}") + return False + + used = laconic.app_deployment_removals( + {"by": payment_address, "payment": tx.hash}, all=True + ) + if len(used): + logger.log( + f"{record.id}: payment {tx.hash} already used on deployment removal {used}" + ) + return False + return True -- 2.45.2 From 3205478878bf12b3f528a321debd0a1aba7b1fef Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Tue, 20 Aug 2024 16:58:34 -0500 Subject: [PATCH 09/13] Undeploy --- .../webapp/__pycache__/__init__.cpython-310.pyc | Bin 176 -> 0 bytes .../__pycache__/deploy_webapp.cpython-310.pyc | Bin 3240 -> 0 bytes .../deploy_webapp_from_registry.cpython-310.pyc | Bin 12782 -> 0 bytes .../__pycache__/run_webapp.cpython-310.pyc | Bin 1452 -> 0 bytes ...ndeploy_webapp_from_registry.cpython-310.pyc | Bin 6295 -> 0 bytes .../webapp/__pycache__/util.cpython-310.pyc | Bin 15737 -> 0 bytes 6 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 stack_orchestrator/deploy/webapp/__pycache__/__init__.cpython-310.pyc delete mode 100644 stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp.cpython-310.pyc delete mode 100644 stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp_from_registry.cpython-310.pyc delete mode 100644 stack_orchestrator/deploy/webapp/__pycache__/run_webapp.cpython-310.pyc delete mode 100644 stack_orchestrator/deploy/webapp/__pycache__/undeploy_webapp_from_registry.cpython-310.pyc delete mode 100644 stack_orchestrator/deploy/webapp/__pycache__/util.cpython-310.pyc diff --git a/stack_orchestrator/deploy/webapp/__pycache__/__init__.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index a3247a5410592fadbfb660c30a3c66407bb144d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176 zcmd1j<>g`kf`rDcX(0MBh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o10aKO;XkRlg)P zCowrYwNgJhwJ2G?xCF@2%`ZyMNG&cYN-W7Qf(XT<2&JSJ16fwg1SD7(0N&#*U;qFB diff --git a/stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp.cpython-310.pyc deleted file mode 100644 index d4f90fda0497a83b856e79d0a64becbc70b91a8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3240 zcmai0PjBPK73c83C|Q!V-nDnVY1qv+3pJpX21UDdgP_0$IrLxz*Z^&HQEAS|qD_kI zaJ04v>QHD;1s3@LZ2-Hset>?IUgp|UKS6=CNc!H8w&iV$kl_4zGxKKVz2EyiW?NgH z2G4JQ|JUi?2AcMF>})za%S^z}qfjj=&cW+Y~6jV;wS6FYUr zj%r(po3_TS)Ej%MZzt`vGw!5Y<1N*9l5W}?_f*?W`swy~8`>>4pdK4cP1>fN*XH;h z-2zEhNqV#ok{!BD2d}m9eWqPLxYC$*x=ZiTo!8o$tjTzf?OpCE=|g&-KA^jAbeQpw z?$L)g?MHN1e?42apahOc9T*TSD z_y~r(@jMnFU_3;b>_?o1g563$@%=_dS)OFeNtDe6yAUw2^Qn@&$kH^N(-*Z|cI$a5 zn~MpJxwPUmJYkWJXKx+{Y8Sp2@D+c6LTD8jcBNPPC831sSNh5jdS$H1mxGmAnU}_u zwz4Yg(xgU3VAT2*fWm}#4e*$|Khlc#&I!@*{SAD$mYQq*%kIjq>`PdA1*4m5sPgsc+m8Nb9Nl&c_6^X30Uc1nI`3a#CSAnM$k54JZJnc z$)Yfkp@b7Z3z1osvpFC4U4;fLiB0Ea!w8AThtP04s*V zk0}5vfSJC+iUpVf?^nhy)SJFnco}Va9@kMz;l-((OIY=P@bb&eRSGZmQR#cWKNBJk za-Ll*525G#fj`R%5#T)!eKiB6JX;7>)HCW?=+#62+1F%LK7FQEqTKhDsz79ier@%` z=O3z3-UYJ&P7E}ti+dd!ry^HdWtLP1ne$*c;boLW`cg% z^o~;Y0GI7zv4;gBsxBTuOZFNBfGV~883+s9$<=jY>gG3H)BU&Wc*G^%EexK*gb1Vm z`az^)8c()`(`&4lrvYQX==9fJ`OfZ(0K5%RyiqdJgpfr&mJ{ua+&N+j4k#@0|s+zKd z30C%+nb&+GjkD*)aH}Q^yH>|7FbX%JkS2PXx3F(xfp+9*QI0WFhnSuf(&jA9Ua?_E z8j-lTWt18N?X$%(!@!-!CqVzm%@fW};OKlQT{eHE0+jE7N~w#fLXWf*MaTyY5daAx z<6ozOx3RH(Ct;weeg@i#kD+iKSMQUqZUaVieTSIe>V{6 zXuK(m73fFL?tmHiPoY_E1p>5)Qhj{Nd+3dlgeWaxL+Z-u-FDK>}FWxB;YNbH<58zYH;n;^5=K7)cMI*bNuT$K7_GSD`s(_G)Kvz5R9X`TQdcK_;UC{{UU7atxi?= z@f^$q%V83WAg?UH80!2vTuZu~{cZtLS~|_?%3gf{ku`csD+u5aWd~0J(t`0IXqANqizrA)K&y(^eHB7;jw>NBz0PY7$ zjHk-~D(RyGYSV#V5s1LS%Dp!UTZI=tfFVA@;zKOna=u+}nZr6c0%M=~S_rGh}L@aYwaAbOE0MjV4s<{hoDXkO8jJfROVNu+$3Pyyq>z`F+ zrVVQk+4sY3<@;5+ru9aeEYyPSPzUr+a1_}32oa&NmsoPc;qqp4|-o< AumAu6 diff --git a/stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp_from_registry.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/deploy_webapp_from_registry.cpython-310.pyc deleted file mode 100644 index dbd1a31d2bc8464a75bd13c96a309cd5ea7b6704..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12782 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 diff --git a/stack_orchestrator/deploy/webapp/__pycache__/run_webapp.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/run_webapp.cpython-310.pyc deleted file mode 100644 index 0ea5bdf4c03e53f1b0bb1a6c17a27008e9131ad2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1452 zcmZWp&2HQ_5Ek{nTCMFmgd&K>_>N2k1NW8F=j}uMiYPhmsdaPzszsa)vYWkz%8fr{Q@1>!0)aK-2zK z%G<+1YY5IJ2&!>?$W*AvKp});v!FlOKYyq|4>QZn+ZN0TkzOO#& zS~2|j`)|LQPNV5hPmZO5jg@;Ph?k6?Ml}eLm5Z9+Qr=d@RUvvt!;v%+aW1XyqDyz% z3u&tQ_`ceGxGJ+3D~OVORSVz|g*xF`B^bxIfEi=c`W5N+*#Pg4QLn6rPb9|?WMYy7@*k^xuz&yn diff --git a/stack_orchestrator/deploy/webapp/__pycache__/undeploy_webapp_from_registry.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/undeploy_webapp_from_registry.cpython-310.pyc deleted file mode 100644 index 49942936420c49ef5aec07d231162171797c532f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6295 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 E0$M&Dp#T5? diff --git a/stack_orchestrator/deploy/webapp/__pycache__/util.cpython-310.pyc b/stack_orchestrator/deploy/webapp/__pycache__/util.cpython-310.pyc deleted file mode 100644 index 0cca3e338f8b170a1bcc35e8c106882e2d683161..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15737 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 LeeV^okk9{rW^+TN -- 2.45.2 From 6e64b602955d71eec705a5426dddd18afb0286bd Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Tue, 20 Aug 2024 18:02:09 -0500 Subject: [PATCH 10/13] Set payment address --- .../deploy/webapp/undeploy_webapp_from_registry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py index 631a2fef..31cf115f 100644 --- a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py @@ -211,7 +211,9 @@ def command( # noqa: C901 include_tags = [tag.strip() for tag in include_tags.split(",") if tag] exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag] - laconic = LaconicRegistryClient(laconic_config) + laconic = LaconicRegistryClient(laconic_config, log_file=sys.stderr) + if not payment_address: + payment_address = laconic.whoami().address # Find deployment removal requests. # single request -- 2.45.2 From e06de5f4b72cfda092889a2c3e1c70563d158ca4 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Tue, 20 Aug 2024 18:19:22 -0500 Subject: [PATCH 11/13] Include payment in removal record --- .../deploy/webapp/undeploy_webapp_from_registry.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py index 31cf115f..7dd6a9d5 100644 --- a/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/undeploy_webapp_from_registry.py @@ -38,6 +38,7 @@ def process_app_removal_request( deployment_parent_dir, delete_volumes, delete_names, + payment_address, ): deployment_record = laconic.get_record( app_removal_request.attributes.deployment, require=True @@ -83,8 +84,13 @@ def process_app_removal_request( "version": "1.0.0", "request": app_removal_request.id, "deployment": deployment_record.id, + "by": payment_address, } } + + if app_removal_request.attributes.payment: + removal_record["record"]["payment"] = app_removal_request.attributes.payment + laconic.publish(removal_record) if delete_names: @@ -330,6 +336,7 @@ def command( # noqa: C901 os.path.abspath(deployment_parent_dir), delete_volumes, delete_names, + payment_address, ) except Exception as e: main_logger.log(f"ERROR processing removal request {r.id}: {e}") -- 2.45.2 From 35d23ca858977203c3d3fc85209b13e5e19017bf Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Tue, 20 Aug 2024 19:01:58 -0500 Subject: [PATCH 12/13] lint --- stack_orchestrator/deploy/webapp/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 0d8c3317..6f6e0937 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -361,7 +361,7 @@ class LaconicRegistryClient: try: parsed = AttrDict(json.loads(logged_cmd(self.log_file, *args))) except: - pass + pass # noqa: E722 if parsed: self.cache["txs"][txHash] = parsed -- 2.45.2 From 578fd933a72b86ca1192986a88fdde1b7741534f Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Tue, 20 Aug 2024 19:10:27 -0500 Subject: [PATCH 13/13] lint --- stack_orchestrator/deploy/webapp/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 6f6e0937..e757f5d5 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -360,8 +360,8 @@ class LaconicRegistryClient: parsed = None try: parsed = AttrDict(json.loads(logged_cmd(self.log_file, *args))) - except: - pass # noqa: E722 + except: # noqa: E722 + pass if parsed: self.cache["txs"][txHash] = parsed -- 2.45.2