diff --git a/stack_orchestrator/deploy/k8s/cluster_info.py b/stack_orchestrator/deploy/k8s/cluster_info.py index be1b2e3d..838fac08 100644 --- a/stack_orchestrator/deploy/k8s/cluster_info.py +++ b/stack_orchestrator/deploy/k8s/cluster_info.py @@ -114,22 +114,27 @@ class ClusterInfo: nodeports.append(service) return nodeports - def get_ingress(self, use_tls=False, certificate=None, cluster_issuer="letsencrypt-prod"): + def get_ingress(self, use_tls=False, certificate_by_host={}, cluster_issuer="letsencrypt-prod"): # No ingress for a deployment that has no http-proxy defined, for now http_proxy_info_list = self.spec.get_http_proxy() - ingress = None - if http_proxy_info_list: - # TODO: handle multiple definitions - http_proxy_info = http_proxy_info_list[0] + if not http_proxy_info_list: + return None + + tls = [] if use_tls else None + rules = [] + for http_proxy_info in http_proxy_info_list: if opts.o.debug: print(f"http-proxy: {http_proxy_info}") # TODO: good enough parsing for webapp deployment for now host_name = http_proxy_info["host-name"] - rules = [] - tls = [client.V1IngressTLS( - hosts=certificate["spec"]["dnsNames"] if certificate else [host_name], - secret_name=certificate["spec"]["secretName"] if certificate else f"{self.app_name}-tls" - )] if use_tls else None + certificate = certificate_by_host[host_name] if host_name in certificate_by_host else None + + if use_tls: + tls.append(client.V1IngressTLS( + hosts=certificate["spec"]["dnsNames"] if certificate else [host_name], + secret_name=certificate["spec"]["secretName"] if certificate else f"{self.app_name}-{host_name}-tls" + )) + paths = [] for route in http_proxy_info["routes"]: path = route["path"] @@ -156,24 +161,24 @@ class ClusterInfo: paths=paths ) )) - spec = client.V1IngressSpec( - tls=tls, - rules=rules - ) + spec = client.V1IngressSpec( + tls=tls, + rules=rules + ) - ingress_annotations = { - "kubernetes.io/ingress.class": "nginx", - } - if not certificate: - ingress_annotations["cert-manager.io/cluster-issuer"] = cluster_issuer + ingress_annotations = { + "kubernetes.io/ingress.class": "nginx", + } + if not certificate: + ingress_annotations["cert-manager.io/cluster-issuer"] = cluster_issuer - ingress = client.V1Ingress( - metadata=client.V1ObjectMeta( - name=f"{self.app_name}-ingress", - annotations=ingress_annotations - ), - spec=spec - ) + ingress = client.V1Ingress( + metadata=client.V1ObjectMeta( + name=f"{self.app_name}-ingress", + annotations=ingress_annotations + ), + spec=spec + ) return ingress # TODO: suppoprt multiple services diff --git a/stack_orchestrator/deploy/k8s/deploy_k8s.py b/stack_orchestrator/deploy/k8s/deploy_k8s.py index b254fd4c..c2df1bd1 100644 --- a/stack_orchestrator/deploy/k8s/deploy_k8s.py +++ b/stack_orchestrator/deploy/k8s/deploy_k8s.py @@ -227,15 +227,18 @@ class K8sDeployer(Deployer): self._create_volume_data() self._create_deployment() - http_proxy_info = self.cluster_info.spec.get_http_proxy() + http_proxy_info_list = self.cluster_info.spec.get_http_proxy() # Note: at present we don't support tls for kind (and enabling tls causes errors) - use_tls = http_proxy_info and not self.is_kind() - certificate = self._find_certificate_for_host_name(http_proxy_info[0]["host-name"]) if use_tls else None - if opts.o.debug: - if certificate: - print(f"Using existing certificate: {certificate}") + use_tls = http_proxy_info_list and not self.is_kind() + certificate_by_host = {} + if use_tls: + for http_proxy_info in http_proxy_info_list: + certificate = self._find_certificate_for_host_name(http_proxy_info["host-name"]) + if opts.o.debug and certificate: + print(f"Using existing certificate: {certificate}") + certificate_by_host[http_proxy_info["host-name"]] = certificate - ingress: client.V1Ingress = self.cluster_info.get_ingress(use_tls=use_tls, certificate=certificate) + ingress: client.V1Ingress = self.cluster_info.get_ingress(use_tls=use_tls, certificate_by_host=certificate_by_host) if ingress: if opts.o.debug: print(f"Sending this ingress: {ingress}") @@ -381,36 +384,46 @@ class K8sDeployer(Deployer): if not pods: return - hostname = "?" - ip = "?" - tls = "?" + tls_by_host = {} try: ingress = self.networking_api.read_namespaced_ingress(namespace=self.k8s_namespace, name=self.cluster_info.get_ingress().metadata.name) - cert = self.custom_obj_api.get_namespaced_custom_object( - group="cert-manager.io", - version="v1", - namespace=self.k8s_namespace, - plural="certificates", - name=ingress.spec.tls[0].secret_name - ) - - hostname = ingress.spec.rules[0].host ip = ingress.status.load_balancer.ingress[0].ip - tls = "notBefore: %s; notAfter: %s; names: %s" % ( - cert["status"]["notBefore"], cert["status"]["notAfter"], ingress.spec.tls[0].hosts - ) + for rule in ingress.spec.rules: + hostname = rule.host + tls_spec = next((tls for tls in ingress.spec.tls if hostname in tls.hosts), None) + if tls_spec: + cert = self.custom_obj_api.get_namespaced_custom_object( + group="cert-manager.io", + version="v1", + namespace=self.k8s_namespace, + plural="certificates", + name=tls_spec.secret_name + ) + tls = "notBefore: %s; notAfter: %s; names: %s" % ( + cert["status"]["notBefore"], cert["status"]["notAfter"], tls_spec.hosts + ) + tls_by_host[hostname] = tls + else: + tls_by_host[hostname] = None except: # noqa: E722 pass print("Ingress:") - print("\tHostname:", hostname) - print("\tIP:", ip) - print("\tTLS:", tls) - print("") - print("Pods:") + if len(tls_by_host) == 0: + print("\tHostname:", "?") + print("\tIP:", "?") + print("\tTLS:", "?") + print("") + for hostname, tls in tls_by_host.items(): + print("\tHostname:", hostname) + print("\tIP:", ip) + print("\tTLS:", tls) + print("") + + print("Pods:") for p in pods: if p.metadata.deletion_timestamp: print(f"\t{p.metadata.namespace}/{p.metadata.name}: Terminating ({p.metadata.deletion_timestamp})") diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp.py b/stack_orchestrator/deploy/webapp/deploy_webapp.py index 4c91dec3..871f227e 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp.py @@ -13,12 +13,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import shutil +from typing import List import click import os +import yaml from pathlib import Path from urllib.parse import urlparse from tempfile import NamedTemporaryFile +from stack_orchestrator import constants from stack_orchestrator.util import error_exit, global_options2 from stack_orchestrator.deploy.deployment_create import init_operation, create_operation from stack_orchestrator.deploy.deploy import create_deploy_context @@ -36,25 +40,38 @@ def _fixup_container_tag(deployment_dir: str, image: str): wfile.write(contents) -def _fixup_url_spec(spec_file_name: str, url: str): - # url is like: https://example.com/path - parsed_url = urlparse(url) - http_proxy_spec = f''' - http-proxy: - - host-name: {parsed_url.hostname} - routes: - - path: '{parsed_url.path if parsed_url.path else "/"}' - proxy-to: webapp:80 - ''' +def _fixup_url_spec(spec_file_name: str, urls: List[str]): spec_file_path = Path(spec_file_name) - with open(spec_file_path) as rfile: - contents = rfile.read() - contents = contents + http_proxy_spec - with open(spec_file_path, "w") as wfile: - wfile.write(contents) + + # Load existing spec + with open(spec_file_path, "r") as file: + spec_data = yaml.safe_load(file) or {} + + # Build new http-proxy entries + http_proxy_entries = [] + for url in urls: + parsed_url = urlparse(url) + http_proxy_entries.append({ + "host-name": parsed_url.hostname, + "routes": [ + { + "path": parsed_url.path if parsed_url.path else "/", + "proxy-to": "webapp:80" + } + ] + }) + + # Update the spec + if "network" not in spec_data: + spec_data["network"] = {} + spec_data["network"]["http-proxy"] = http_proxy_entries + + # Write back the updated YAML + with open(spec_file_path, "w") as file: + yaml.dump(spec_data, file, default_flow_style=False, sort_keys=False) -def create_deployment(ctx, deployment_dir, image, url, kube_config, image_registry, env_file): +def create_deployment(ctx, deployment_dir, image, urls, 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) @@ -86,7 +103,7 @@ def create_deployment(ctx, deployment_dir, image, url, kube_config, image_regist None ) # Add the TLS and DNS spec - _fixup_url_spec(spec_file_name, url) + _fixup_url_spec(spec_file_name, urls) create_operation( deploy_command_context, spec_file_name, @@ -99,6 +116,20 @@ def create_deployment(ctx, deployment_dir, image, url, kube_config, image_regist os.remove(spec_file_name) +def update_deployment(deployment_dir, image, urls, env_file): + # Update config if required + if env_file: + deployment_config_file = os.path.join(deployment_dir, "config.env") + shutil.copyfile(env_file, deployment_config_file) + + # Update existing deployment spec with new urls + _fixup_url_spec(os.path.join(deployment_dir, constants.spec_file_name), urls) + + # Update the image name if required + if image: + _fixup_container_tag(deployment_dir, image) + + @click.group() @click.pass_context def command(ctx): @@ -120,4 +151,4 @@ def command(ctx): 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) + return create_deployment(ctx, deployment_dir, image, [url], kube_config, image_registry, env_file) diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index 24a529c2..c8cf0ced 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -20,6 +20,7 @@ import shutil import sys import tempfile import time +from typing import List import uuid import yaml @@ -38,7 +39,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, @@ -47,7 +49,7 @@ from stack_orchestrator.deploy.webapp.util import ( ) -def process_app_deployment_request( +def process_app_deployment_request( # noqa ctx, laconic: LaconicRegistryClient, app_deployment_request, @@ -76,44 +78,53 @@ 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 + dns_lrns = [] + existing_dns_records_by_lrns = {} + for fqdn in fqdns: + dns_lrn = f"{dns_record_namespace}/{fqdn}" + dns_lrns.append(dns_lrn) - if matched_owner: - logger.log(f"Matched DnsRecord ownership: {matched_owner}") - else: + dns_record = laconic.get_record(dns_lrn) + existing_dns_records_by_lrns[dns_lrn] = dns_record + + 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 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,30 +156,49 @@ 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 + deployment_dir = os.path.join(deployment_parent_dir, fqdns[-1]) + + # Existing deployment record: take the first lrn that resolves + deployment_record = None + fqdns_to_release = [] + existing_deployment_dir = deployment_dir # Default to target dir in case the app had been undeployed + + for app_deployment_lrn in app_deployment_lrns: + deployment_record = laconic.get_record(app_deployment_lrn) + if deployment_record: + # Determine the deployment dir for existing deployment + dir_name = deployment_record.attributes.url.replace("https://", "") + existing_deployment_dir = os.path.join(deployment_parent_dir, dir_name) + if not os.path.exists(existing_deployment_dir): + raise Exception( + "Deployment record %s exists, but not deployment dir %s. Please remove name." + % (app_deployment_lrn, existing_deployment_dir) + ) + + previous_app_deployment_lrns: List[str] = deployment_record.names + previous_fqdns = [lrn.removeprefix(f"{deployment_record_namespace}/") for lrn in previous_app_deployment_lrns] + fqdns_to_release = list(set(previous_fqdns) - set(fqdns)) + + 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_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 deployment_record: - raise Exception( - "Deployment record %s exists, but not deployment dir %s. Please remove name." - % (app_deployment_lrn, deployment_dir) - ) + if not os.path.exists(existing_deployment_dir): logger.log( f"Creating webapp deployment in: {deployment_dir} with container id: {deployment_container_tag}" ) @@ -176,13 +206,23 @@ def process_app_deployment_request( ctx, deployment_dir, deployment_container_tag, - f"https://{fqdn}", + [f"https://{fqdn}" for fqdn in fqdns], kube_config, image_registry, env_filename, ) - elif env_filename: - shutil.copyfile(env_filename, deployment_config_file) + else: + # Rename deployment dir according to new request (last fqdn from given dns) + os.rename(existing_deployment_dir, deployment_dir) + + # Update the image name deployment_container_tag + # Skip for redeployment as deployment_container_tag won't get built + updated_image = None + if deployment_record.attributes.application != app.id: + updated_image = deployment_container_tag + + # Update the existing deployment + deploy_webapp.update_deployment(deployment_dir, updated_image, [f"https://{fqdn}" for fqdn in fqdns], env_filename) needs_k8s_deploy = False if force_rebuild: @@ -232,10 +272,11 @@ def process_app_deployment_request( else: logger.log("Requested app is already deployed, skipping build and image push") - # 7. update config (if needed) + # 7. restart deployment on config or url spec change if ( not deployment_record - or file_hash(deployment_config_file) != deployment_record.attributes.meta.config + or file_hash(os.path.join(deployment_dir, "config.env")) != deployment_record.attributes.meta.config + or len(fqdns_to_release) != 0 ): needs_k8s_deploy = True @@ -248,9 +289,10 @@ def process_app_deployment_request( laconic, app, deployment_record, - app_deployment_lrn, - dns_record, - dns_lrn, + app_deployment_lrns, + existing_dns_records_by_lrns, + dns_lrns, + dns_record_namespace, deployment_dir, dns_value, app_deployment_request, @@ -258,6 +300,17 @@ def process_app_deployment_request( logger, ) logger.log("Publication complete.") + + # 9. delete unused names from previous deployment (app and dns) + for fqdn in fqdns_to_release: + # Delete app deployment name and DNS name + deployment_name = "{deployment_record_namespace}/{fqdn}" + dns_name = "{deployment_record_namespace}/{fqdn}" + + logger.log(f"Removing names {deployment_name} and {dns_name}") + laconic.delete_name(deployment_name) + laconic.delete_name(dns_name) + logger.log("END - process_app_deployment_request") @@ -517,21 +570,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 +597,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..2ba79149 100644 --- a/stack_orchestrator/deploy/webapp/publish_webapp_deployer.py +++ b/stack_orchestrator/deploy/webapp/publish_webapp_deployer.py @@ -76,6 +76,7 @@ def command( # noqa: C901 "name": hostname, "publicKey": pub_key, "paymentAddress": payment_address, + "deployerVersion": "1.0.0", } } diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 991dd249..ff5131a6 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -21,6 +21,7 @@ import random import subprocess import sys import tempfile +from typing import List import uuid import yaml @@ -685,9 +686,10 @@ def publish_deployment( laconic: LaconicRegistryClient, app_record, deploy_record, - deployment_lrn, - dns_record, - dns_lrn, + deployment_lrns, + existing_dns_records_by_lrns, + dns_lrns: List[str], + dns_record_namespace, deployment_dir, dns_value=None, app_deployment_request=None, @@ -701,42 +703,48 @@ def publish_deployment( int(deploy_record.attributes.version.split(".")[-1]) + 1 ) - if not dns_record: - dns_ver = "0.0.1" - else: - dns_ver = "0.0.%d" % (int(dns_record.attributes.version.split(".")[-1]) + 1) + dns_ids = [] + for dns_lrn in dns_lrns: + dns_record = existing_dns_records_by_lrns[dns_lrn] + if not dns_record: + dns_ver = "0.0.1" + else: + dns_ver = "0.0.%d" % (int(dns_record.attributes.version.split(".")[-1]) + 1) + + fqdn = dns_lrn.removeprefix(f"{dns_record_namespace}/") + uniq = uuid.uuid4() + + new_dns_record = { + "record": { + "type": "DnsRecord", + "version": dns_ver, + "name": fqdn, + "resource_type": "A", + "meta": {"so": uniq.hex}, + } + } + if app_deployment_request: + new_dns_record["record"]["request"] = app_deployment_request.id + if dns_value: + new_dns_record["record"]["value"] = dns_value + + if logger: + logger.log("Publishing DnsRecord.") + dns_id = laconic.publish(new_dns_record, [dns_lrn]) + dns_ids.append(dns_id) spec = yaml.full_load(open(os.path.join(deployment_dir, "spec.yml"))) - fqdn = spec["network"]["http-proxy"][0]["host-name"] - - uniq = uuid.uuid4() - - new_dns_record = { - "record": { - "type": "DnsRecord", - "version": dns_ver, - "name": fqdn, - "resource_type": "A", - "meta": {"so": uniq.hex}, - } - } - if app_deployment_request: - new_dns_record["record"]["request"] = app_deployment_request.id - if dns_value: - new_dns_record["record"]["value"] = dns_value - - if logger: - logger.log("Publishing DnsRecord.") - dns_id = laconic.publish(new_dns_record, [dns_lrn]) + last_fqdn = spec["network"]["http-proxy"][-1]["host-name"] + last_dns_id = dns_ids[-1] new_deployment_record = { "record": { "type": "ApplicationDeploymentRecord", "version": deploy_ver, - "url": f"https://{fqdn}", + "url": f"https://{last_fqdn}", "name": app_record.attributes.name, "application": app_record.id, - "dns": dns_id, + "dns": last_dns_id, "meta": { "config": file_hash(os.path.join(deployment_dir, "config.env")), "so": uniq.hex, @@ -758,21 +766,34 @@ def publish_deployment( if logger: logger.log("Publishing ApplicationDeploymentRecord.") - deployment_id = laconic.publish(new_deployment_record, [deployment_lrn]) - return {"dns": dns_id, "deployment": deployment_id} + deployment_id = laconic.publish(new_deployment_record, deployment_lrns) + return {"dns": dns_ids, "deployment": deployment_id} -def hostname_for_deployment_request(app_deployment_request, laconic): - dns_name = app_deployment_request.attributes.dns - if not dns_name: +def get_requested_names(app_deployment_request): + request_dns = app_deployment_request.attributes.dns + return request_dns.split(",") if request_dns else [] + + +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):