diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index 24a529c2..69407580 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -38,7 +38,8 @@ from stack_orchestrator.deploy.webapp.util import ( file_hash, deploy_to_k8s, publish_deployment, - hostname_for_deployment_request, + get_requested_names, + hostnames_for_deployment_request, generate_hostname_for_app, match_owner, skip_by_tag, @@ -76,44 +77,47 @@ def process_app_deployment_request( logger.log(f"Retrieved app record {app_deployment_request.attributes.application}") # 2. determine dns - requested_name = hostname_for_deployment_request(app_deployment_request, laconic) - logger.log(f"Determined requested name: {requested_name}") + requested_names = hostnames_for_deployment_request(app_deployment_request, laconic) + logger.log(f"Determined requested name(s): {','.join(str(x) for x in requested_names)}") - if "." in requested_name: - if "allow" == fqdn_policy or "preexisting" == fqdn_policy: - fqdn = requested_name + fqdns = [] + for requested_name in requested_names: + if "." in requested_name: + if "allow" == fqdn_policy or "preexisting" == fqdn_policy: + fqdns.append(requested_name) + else: + raise Exception( + f"{requested_name} is invalid: only unqualified hostnames are allowed." + ) else: - raise Exception( - f"{requested_name} is invalid: only unqualified hostnames are allowed." - ) - else: - fqdn = f"{requested_name}.{default_dns_suffix}" + fqdns.append(f"{requested_name}.{default_dns_suffix}") # Normalize case (just in case) - fqdn = fqdn.lower() + fqdns = [fqdn.lower() for fqdn in fqdns] - # 3. check ownership of existing dnsrecord vs this request - dns_lrn = f"{dns_record_namespace}/{fqdn}" - dns_record = laconic.get_record(dns_lrn) - if dns_record: - matched_owner = match_owner(app_deployment_request, dns_record) - if not matched_owner and dns_record.attributes.request: - matched_owner = match_owner( - app_deployment_request, - laconic.get_record(dns_record.attributes.request, require=True), - ) + # 3. check ownership of existing dnsrecord(s) vs this request + for fqdn in fqdns: + dns_lrn = f"{dns_record_namespace}/{fqdn}" + dns_record = laconic.get_record(dns_lrn) + if dns_record: + matched_owner = match_owner(app_deployment_request, dns_record) + if not matched_owner and dns_record.attributes.request: + matched_owner = match_owner( + app_deployment_request, + laconic.get_record(dns_record.attributes.request, require=True), + ) - if matched_owner: - logger.log(f"Matched DnsRecord ownership: {matched_owner}") - else: + if matched_owner: + logger.log(f"Matched DnsRecord ownership for {fqdn}: {matched_owner}") + else: + raise Exception( + "Unable to confirm ownership of DnsRecord %s for request %s" + % (dns_lrn, app_deployment_request.id) + ) + elif "preexisting" == fqdn_policy: raise Exception( - "Unable to confirm ownership of DnsRecord %s for request %s" - % (dns_lrn, app_deployment_request.id) + f"No pre-existing DnsRecord {dns_lrn} could be found for request {app_deployment_request.id}." ) - elif "preexisting" == fqdn_policy: - raise Exception( - f"No pre-existing DnsRecord {dns_lrn} could be found for request {app_deployment_request.id}." - ) # 4. get build and runtime config from request env = {} @@ -145,25 +149,39 @@ def process_app_deployment_request( # 5. determine new or existing deployment # a. check for deployment lrn - app_deployment_lrn = f"{deployment_record_namespace}/{fqdn}" + app_deployment_lrns = [f"{deployment_record_namespace}/{fqdn}" for fqdn in fqdns] if app_deployment_request.attributes.deployment: - app_deployment_lrn = app_deployment_request.attributes.deployment - if not app_deployment_lrn.startswith(deployment_record_namespace): - raise Exception( - "Deployment LRN %s is not in a supported namespace" - % app_deployment_request.attributes.deployment - ) + app_deployment_lrns = [app_deployment_request.attributes.deployment] + if not app_deployment_lrns[0].startswith(deployment_record_namespace): + raise Exception( + "Deployment LRN %s is not in a supported namespace" + % app_deployment_request.attributes.deployment + ) - deployment_record = laconic.get_record(app_deployment_lrn) - deployment_dir = os.path.join(deployment_parent_dir, fqdn) + # Target deployment dir and existing deployment dir + deployment_dir = os.path.join(deployment_parent_dir, fqdns[-1]) + existing_deployment_dir = deployment_dir + + # Existing deployment record: take the first lrn that resolves + deployment_record = None + for app_deployment_lrn in app_deployment_lrns: + deployment_record = laconic.get_record(app_deployment_lrn) + if deployment_record is not None: + # TODO: Determine the deployment dir for existing deployment + # prev_request = laconic.get_record(deployment_record.attributes.request, True) + # existing_deployment_dir = ... + + break + + # Use the last fqdn for unique deployment container tag # At present we use this to generate a unique but stable ID for the app's host container # TODO: implement support to derive this transparently from the already-unique deployment id - unique_deployment_id = hashlib.md5(fqdn.encode()).hexdigest()[:16] - deployment_config_file = os.path.join(deployment_dir, "config.env") + unique_deployment_id = hashlib.md5(fqdns[-1].encode()).hexdigest()[:16] + deployment_config_file = os.path.join(existing_deployment_dir, "config.env") deployment_container_tag = "laconic-webapp/%s:local" % unique_deployment_id app_image_shared_tag = f"laconic-webapp/{app.id}:local" # b. check for deployment directory (create if necessary) - if not os.path.exists(deployment_dir): + if not os.path.exists(existing_deployment_dir): if deployment_record: raise Exception( "Deployment record %s exists, but not deployment dir %s. Please remove name." @@ -172,11 +190,12 @@ def process_app_deployment_request( logger.log( f"Creating webapp deployment in: {deployment_dir} with container id: {deployment_container_tag}" ) + # TODO: Pass URLs for all fqdns deploy_webapp.create_deployment( ctx, deployment_dir, deployment_container_tag, - f"https://{fqdn}", + f"https://{fqdns[-1]}", kube_config, image_registry, env_filename, @@ -184,6 +203,16 @@ def process_app_deployment_request( elif env_filename: shutil.copyfile(env_filename, deployment_config_file) + # TODO: Update spec with new urls + + # TODO: Update with deployment_container_tag if it's not a redeployment + # as for redeployment, deployment_container_tag won't get built + # if deployment_record.attributes.application != app.id: + # ... + + # Rename deployment dir according to new request (last fqdn from given dns) + os.rename(existing_deployment_dir, deployment_dir) + needs_k8s_deploy = False if force_rebuild: logger.log( @@ -233,17 +262,24 @@ def process_app_deployment_request( logger.log("Requested app is already deployed, skipping build and image push") # 7. update config (if needed) + # TODO: Also check if domains set has changed if ( not deployment_record or file_hash(deployment_config_file) != deployment_record.attributes.meta.config ): needs_k8s_deploy = True - # 8. update k8s deployment + # TODO: 8. delete unused names + # if deployment_record: + # ... + + # 9. update k8s deployment if needs_k8s_deploy: deploy_to_k8s(deployment_record, deployment_dir, recreate_on_deploy, logger) logger.log("Publishing deployment to registry.") + # TODO: Publish multiple DNS records + # TODO: Point all app_deployment_lrns publish_deployment( laconic, app, @@ -517,21 +553,26 @@ def command( # noqa: C901 result = "ERROR" continue - requested_name = r.attributes.dns - if not requested_name: - requested_name = generate_hostname_for_app(app) + requested_names = get_requested_names(r) + if len(requested_names) == 0: + requested_names = [generate_hostname_for_app(app)] main_logger.log( - "Generating name %s for request %s." % (requested_name, r.id) + "Generating name %s for request %s." % (requested_names[0], r.id) ) - if ( - requested_name in skipped_by_name - or requested_name in requests_by_name - ): - main_logger.log( - "Ignoring request %s, it has been superseded." % r.id - ) - result = "SKIP" + # Skip request if any of the names is superseded + for requested_name in requested_names: + if ( + requested_name in skipped_by_name + or requested_name in requests_by_name + ): + main_logger.log( + "Ignoring request %s, it has been superseded." % r.id + ) + result = "SKIP" + break + + if result == "SKIP": continue if skip_by_tag(r, include_tags, exclude_tags): @@ -539,15 +580,20 @@ def command( # noqa: C901 "Skipping request %s, filtered by tag (include %s, exclude %s, present %s)" % (r.id, include_tags, exclude_tags, r.attributes.tags) ) - skipped_by_name[requested_name] = r + + for requested_name in requested_names: + skipped_by_name[requested_name] = r + result = "SKIP" continue main_logger.log( "Found pending request %s to run application %s on %s." - % (r.id, r.attributes.application, requested_name) + % (r.id, r.attributes.application, ','.join(str(x) for x in requested_names)) ) - requests_by_name[requested_name] = r + + # Set request for on of the names + requests_by_name[requested_names[-1]] = r except Exception as e: result = "ERROR" main_logger.log(f"ERROR examining request {r.id}: " + str(e)) diff --git a/stack_orchestrator/deploy/webapp/publish_webapp_deployer.py b/stack_orchestrator/deploy/webapp/publish_webapp_deployer.py index 851e90e1..dbbe7d6f 100644 --- a/stack_orchestrator/deploy/webapp/publish_webapp_deployer.py +++ b/stack_orchestrator/deploy/webapp/publish_webapp_deployer.py @@ -78,6 +78,7 @@ def command( # noqa: C901 "paymentAddress": payment_address, } } + # TODO: Add deployerVersion 1.0.0 if min_required_payment: webapp_deployer_record["record"][ diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 991dd249..ebcd2526 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -761,18 +761,29 @@ def publish_deployment( deployment_id = laconic.publish(new_deployment_record, [deployment_lrn]) return {"dns": dns_id, "deployment": deployment_id} +def get_requested_names(app_deployment_request): + request_dns = app_deployment_request.attributes.dns + return request_dns.split(",") if request_dns else [] -def hostname_for_deployment_request(app_deployment_request, laconic): - dns_name = app_deployment_request.attributes.dns - if not dns_name: +def hostnames_for_deployment_request(app_deployment_request, laconic): + requested_names = get_requested_names(app_deployment_request) + + if len(requested_names) == 0: 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) - dns_name = record.attributes.name - return dns_name + return [generate_hostname_for_app(app)] + + dns_names = [] + for requested_name in requested_names: + dns_name = requested_name + if dns_name.startswith("lrn://"): + record = laconic.get_record(dns_name, require=True) + dns_name = record.attributes.name + + dns_names.append(dns_name) + + return dns_names def generate_hostname_for_app(app):