forked from cerc-io/stack-orchestrator
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			pm-multipl
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 34f3b719e4 | |||
| 0e814bd4da | 
| @ -26,8 +26,14 @@ fi | |||||||
| SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) | ||||||
| WORK_DIR="${1:-/app}" | WORK_DIR="${1:-/app}" | ||||||
| 
 | 
 | ||||||
|  | if [ -f "${WORK_DIR}/build-webapp.sh" ]; then | ||||||
|  |   echo "Building webapp with ${WORK_DIR}/build-webapp.sh ..." | ||||||
| cd "${WORK_DIR}" || exit 1 | cd "${WORK_DIR}" || exit 1 | ||||||
| 
 | 
 | ||||||
|  |   ./build-webapp.sh || exit 1 | ||||||
|  |   exit 0 | ||||||
|  | fi | ||||||
|  | 
 | ||||||
| if [ -f "next.config.mjs" ]; then | if [ -f "next.config.mjs" ]; then | ||||||
|   NEXT_CONFIG_JS="next.config.mjs" |   NEXT_CONFIG_JS="next.config.mjs" | ||||||
|   IMPORT_OR_REQUIRE="import" |   IMPORT_OR_REQUIRE="import" | ||||||
|  | |||||||
| @ -30,6 +30,13 @@ fi | |||||||
| CERC_WEBAPP_FILES_DIR="${CERC_WEBAPP_FILES_DIR:-/app}" | CERC_WEBAPP_FILES_DIR="${CERC_WEBAPP_FILES_DIR:-/app}" | ||||||
| cd "$CERC_WEBAPP_FILES_DIR" | cd "$CERC_WEBAPP_FILES_DIR" | ||||||
| 
 | 
 | ||||||
|  | if [ -f "./run-webapp.sh" ]; then | ||||||
|  |   echo "Running webapp with run-webapp.sh ..." | ||||||
|  |   cd "${WORK_DIR}" || exit 1 | ||||||
|  |   ./run-webapp.sh & | ||||||
|  |   tpid=$! | ||||||
|  |   wait $tpid | ||||||
|  | else  | ||||||
|   "$SCRIPT_DIR/apply-runtime-env.sh" "`pwd`" .next .next-r |   "$SCRIPT_DIR/apply-runtime-env.sh" "`pwd`" .next .next-r | ||||||
|   mv .next .next.old |   mv .next .next.old | ||||||
|   mv .next-r/.next . |   mv .next-r/.next . | ||||||
| @ -63,3 +70,4 @@ if [ "$CERC_NEXTJS_SKIP_GENERATE" != "true" ]; then | |||||||
|   fi |   fi | ||||||
| 
 | 
 | ||||||
|   $CERC_BUILD_TOOL start . -- -p ${CERC_LISTEN_PORT:-80} |   $CERC_BUILD_TOOL start . -- -p ${CERC_LISTEN_PORT:-80} | ||||||
|  | fi | ||||||
|  | |||||||
| @ -56,7 +56,6 @@ class DeploymentContext: | |||||||
|         self.stack.init_from_file(self.get_stack_file()) |         self.stack.init_from_file(self.get_stack_file()) | ||||||
|         deployment_file_path = self.get_deployment_file() |         deployment_file_path = self.get_deployment_file() | ||||||
|         if deployment_file_path.exists(): |         if deployment_file_path.exists(): | ||||||
|             with deployment_file_path: |  | ||||||
|             obj = get_yaml().load(open(deployment_file_path, "r")) |             obj = get_yaml().load(open(deployment_file_path, "r")) | ||||||
|             self.id = obj[constants.cluster_id_key] |             self.id = obj[constants.cluster_id_key] | ||||||
|         # Handle the case of a legacy deployment with no file |         # Handle the case of a legacy deployment with no file | ||||||
|  | |||||||
| @ -114,27 +114,22 @@ class ClusterInfo: | |||||||
|                         nodeports.append(service) |                         nodeports.append(service) | ||||||
|         return nodeports |         return nodeports | ||||||
| 
 | 
 | ||||||
|     def get_ingress(self, use_tls=False, certificate_by_host={}, cluster_issuer="letsencrypt-prod"): |     def get_ingress(self, use_tls=False, certificate=None, cluster_issuer="letsencrypt-prod"): | ||||||
|         # No ingress for a deployment that has no http-proxy defined, for now |         # No ingress for a deployment that has no http-proxy defined, for now | ||||||
|         http_proxy_info_list = self.spec.get_http_proxy() |         http_proxy_info_list = self.spec.get_http_proxy() | ||||||
|         if not http_proxy_info_list: |         ingress = None | ||||||
|             return None |         if http_proxy_info_list: | ||||||
| 
 |             # TODO: handle multiple definitions | ||||||
|         tls = [] if use_tls else None |             http_proxy_info = http_proxy_info_list[0] | ||||||
|         rules = [] |  | ||||||
|         for http_proxy_info in http_proxy_info_list: |  | ||||||
|             if opts.o.debug: |             if opts.o.debug: | ||||||
|                 print(f"http-proxy: {http_proxy_info}") |                 print(f"http-proxy: {http_proxy_info}") | ||||||
|             # TODO: good enough parsing for webapp deployment for now |             # TODO: good enough parsing for webapp deployment for now | ||||||
|             host_name = http_proxy_info["host-name"] |             host_name = http_proxy_info["host-name"] | ||||||
|             certificate = certificate_by_host[host_name] if host_name in certificate_by_host else None |             rules = [] | ||||||
| 
 |             tls = [client.V1IngressTLS( | ||||||
|             if use_tls: |  | ||||||
|                 tls.append(client.V1IngressTLS( |  | ||||||
|                 hosts=certificate["spec"]["dnsNames"] if certificate else [host_name], |                 hosts=certificate["spec"]["dnsNames"] if certificate else [host_name], | ||||||
|                     secret_name=certificate["spec"]["secretName"] if certificate else f"{self.app_name}-{host_name}-tls" |                 secret_name=certificate["spec"]["secretName"] if certificate else f"{self.app_name}-tls" | ||||||
|                 )) |             )] if use_tls else None | ||||||
| 
 |  | ||||||
|             paths = [] |             paths = [] | ||||||
|             for route in http_proxy_info["routes"]: |             for route in http_proxy_info["routes"]: | ||||||
|                 path = route["path"] |                 path = route["path"] | ||||||
|  | |||||||
| @ -227,18 +227,15 @@ class K8sDeployer(Deployer): | |||||||
|         self._create_volume_data() |         self._create_volume_data() | ||||||
|         self._create_deployment() |         self._create_deployment() | ||||||
| 
 | 
 | ||||||
|         http_proxy_info_list = self.cluster_info.spec.get_http_proxy() |         http_proxy_info = self.cluster_info.spec.get_http_proxy() | ||||||
|         # Note: at present we don't support tls for kind (and enabling tls causes errors) |         # Note: at present we don't support tls for kind (and enabling tls causes errors) | ||||||
|         use_tls = http_proxy_info_list and not self.is_kind() |         use_tls = http_proxy_info and not self.is_kind() | ||||||
|         certificate_by_host = {} |         certificate = self._find_certificate_for_host_name(http_proxy_info[0]["host-name"]) if use_tls else None | ||||||
|         if use_tls: |         if opts.o.debug: | ||||||
|             for http_proxy_info in http_proxy_info_list: |             if certificate: | ||||||
|                 certificate = self._find_certificate_for_host_name(http_proxy_info["host-name"]) |  | ||||||
|                 if opts.o.debug and certificate: |  | ||||||
|                 print(f"Using existing certificate: {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_by_host=certificate_by_host) |         ingress: client.V1Ingress = self.cluster_info.get_ingress(use_tls=use_tls, certificate=certificate) | ||||||
|         if ingress: |         if ingress: | ||||||
|             if opts.o.debug: |             if opts.o.debug: | ||||||
|                 print(f"Sending this ingress: {ingress}") |                 print(f"Sending this ingress: {ingress}") | ||||||
| @ -384,46 +381,36 @@ class K8sDeployer(Deployer): | |||||||
|         if not pods: |         if not pods: | ||||||
|             return |             return | ||||||
| 
 | 
 | ||||||
|         tls_by_host = {} |         hostname = "?" | ||||||
|  |         ip = "?" | ||||||
|  |         tls = "?" | ||||||
|         try: |         try: | ||||||
|             ingress = self.networking_api.read_namespaced_ingress(namespace=self.k8s_namespace, |             ingress = self.networking_api.read_namespaced_ingress(namespace=self.k8s_namespace, | ||||||
|                                                                   name=self.cluster_info.get_ingress().metadata.name) |                                                                   name=self.cluster_info.get_ingress().metadata.name) | ||||||
| 
 | 
 | ||||||
|             ip = ingress.status.load_balancer.ingress[0].ip |  | ||||||
|             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( |             cert = self.custom_obj_api.get_namespaced_custom_object( | ||||||
|                 group="cert-manager.io", |                 group="cert-manager.io", | ||||||
|                 version="v1", |                 version="v1", | ||||||
|                 namespace=self.k8s_namespace, |                 namespace=self.k8s_namespace, | ||||||
|                 plural="certificates", |                 plural="certificates", | ||||||
|                         name=tls_spec.secret_name |                 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" % ( |             tls = "notBefore: %s; notAfter: %s; names: %s" % ( | ||||||
|                         cert["status"]["notBefore"], cert["status"]["notAfter"], tls_spec.hosts |                 cert["status"]["notBefore"], cert["status"]["notAfter"], ingress.spec.tls[0].hosts | ||||||
|             ) |             ) | ||||||
|                     tls_by_host[hostname] = tls |  | ||||||
|                 else: |  | ||||||
|                     tls_by_host[hostname] = None |  | ||||||
|         except:  # noqa: E722 |         except:  # noqa: E722 | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|         print("Ingress:") |         print("Ingress:") | ||||||
|         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("\tHostname:", hostname) | ||||||
|         print("\tIP:", ip) |         print("\tIP:", ip) | ||||||
|         print("\tTLS:", tls) |         print("\tTLS:", tls) | ||||||
|         print("") |         print("") | ||||||
| 
 |  | ||||||
|         print("Pods:") |         print("Pods:") | ||||||
|  | 
 | ||||||
|         for p in pods: |         for p in pods: | ||||||
|             if p.metadata.deletion_timestamp: |             if p.metadata.deletion_timestamp: | ||||||
|                 print(f"\t{p.metadata.namespace}/{p.metadata.name}: Terminating ({p.metadata.deletion_timestamp})") |                 print(f"\t{p.metadata.namespace}/{p.metadata.name}: Terminating ({p.metadata.deletion_timestamp})") | ||||||
|  | |||||||
| @ -92,7 +92,6 @@ class Spec: | |||||||
|         return self.obj.get(item, default) |         return self.obj.get(item, default) | ||||||
| 
 | 
 | ||||||
|     def init_from_file(self, file_path: Path): |     def init_from_file(self, file_path: Path): | ||||||
|         with file_path: |  | ||||||
|         self.obj = get_yaml().load(open(file_path, "r")) |         self.obj = get_yaml().load(open(file_path, "r")) | ||||||
|         self.file_path = file_path |         self.file_path = file_path | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -27,5 +27,4 @@ class Stack: | |||||||
|         self.name = name |         self.name = name | ||||||
| 
 | 
 | ||||||
|     def init_from_file(self, file_path: Path): |     def init_from_file(self, file_path: Path): | ||||||
|         with file_path: |  | ||||||
|         self.obj = get_yaml().load(open(file_path, "r")) |         self.obj = get_yaml().load(open(file_path, "r")) | ||||||
|  | |||||||
| @ -13,16 +13,12 @@ | |||||||
| # You should have received a copy of the GNU Affero General Public License | # You should have received a copy of the GNU Affero General Public License | ||||||
| # along with this program.  If not, see <http:#www.gnu.org/licenses/>. | # along with this program.  If not, see <http:#www.gnu.org/licenses/>. | ||||||
| 
 | 
 | ||||||
| import shutil |  | ||||||
| from typing import List |  | ||||||
| import click | import click | ||||||
| import os | import os | ||||||
| import yaml |  | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from urllib.parse import urlparse | from urllib.parse import urlparse | ||||||
| from tempfile import NamedTemporaryFile | from tempfile import NamedTemporaryFile | ||||||
| 
 | 
 | ||||||
| from stack_orchestrator import constants |  | ||||||
| from stack_orchestrator.util import error_exit, global_options2 | from stack_orchestrator.util import error_exit, global_options2 | ||||||
| from stack_orchestrator.deploy.deployment_create import init_operation, create_operation | from stack_orchestrator.deploy.deployment_create import init_operation, create_operation | ||||||
| from stack_orchestrator.deploy.deploy import create_deploy_context | from stack_orchestrator.deploy.deploy import create_deploy_context | ||||||
| @ -40,38 +36,25 @@ def _fixup_container_tag(deployment_dir: str, image: str): | |||||||
|         wfile.write(contents) |         wfile.write(contents) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _fixup_url_spec(spec_file_name: str, urls: List[str]): | def _fixup_url_spec(spec_file_name: str, url: str): | ||||||
|     spec_file_path = Path(spec_file_name) |     # url is like: https://example.com/path | ||||||
| 
 |  | ||||||
|     # 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) |     parsed_url = urlparse(url) | ||||||
|         http_proxy_entries.append({ |     http_proxy_spec = f''' | ||||||
|             "host-name": parsed_url.hostname, |   http-proxy: | ||||||
|             "routes": [ |     - host-name: {parsed_url.hostname} | ||||||
|                 { |       routes: | ||||||
|                     "path": parsed_url.path if parsed_url.path else "/", |         - path: '{parsed_url.path if parsed_url.path else "/"}' | ||||||
|                     "proxy-to": "webapp:80" |           proxy-to: webapp:80 | ||||||
|                 } |     ''' | ||||||
|             ] |     spec_file_path = Path(spec_file_name) | ||||||
|         }) |     with open(spec_file_path) as rfile: | ||||||
| 
 |         contents = rfile.read() | ||||||
|     # Update the spec |         contents = contents + http_proxy_spec | ||||||
|     if "network" not in spec_data: |     with open(spec_file_path, "w") as wfile: | ||||||
|         spec_data["network"] = {} |         wfile.write(contents) | ||||||
|     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, urls, kube_config, image_registry, env_file): | def create_deployment(ctx, deployment_dir, image, url, kube_config, image_registry, env_file): | ||||||
|     # Do the equivalent of: |     # Do the equivalent of: | ||||||
|     # 1. laconic-so --stack webapp-template deploy --deploy-to k8s init --output webapp-spec.yml |     # 1. laconic-so --stack webapp-template deploy --deploy-to k8s init --output webapp-spec.yml | ||||||
|     #   --config (eqivalent of the contents of my-config.env) |     #   --config (eqivalent of the contents of my-config.env) | ||||||
| @ -103,7 +86,7 @@ def create_deployment(ctx, deployment_dir, image, urls, kube_config, image_regis | |||||||
|         None |         None | ||||||
|     ) |     ) | ||||||
|     # Add the TLS and DNS spec |     # Add the TLS and DNS spec | ||||||
|     _fixup_url_spec(spec_file_name, urls) |     _fixup_url_spec(spec_file_name, url) | ||||||
|     create_operation( |     create_operation( | ||||||
|         deploy_command_context, |         deploy_command_context, | ||||||
|         spec_file_name, |         spec_file_name, | ||||||
| @ -116,20 +99,6 @@ def create_deployment(ctx, deployment_dir, image, urls, kube_config, image_regis | |||||||
|     os.remove(spec_file_name) |     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.group() | ||||||
| @click.pass_context | @click.pass_context | ||||||
| def command(ctx): | def command(ctx): | ||||||
| @ -151,4 +120,4 @@ def command(ctx): | |||||||
| def create(ctx, deployment_dir, image, url, kube_config, image_registry, env_file): | def create(ctx, deployment_dir, image, url, kube_config, image_registry, env_file): | ||||||
|     '''create a deployment for the specified webapp container''' |     '''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) | ||||||
|  | |||||||
| @ -20,7 +20,6 @@ import shutil | |||||||
| import sys | import sys | ||||||
| import tempfile | import tempfile | ||||||
| import time | import time | ||||||
| from typing import List |  | ||||||
| import uuid | import uuid | ||||||
| import yaml | import yaml | ||||||
| 
 | 
 | ||||||
| @ -39,8 +38,7 @@ from stack_orchestrator.deploy.webapp.util import ( | |||||||
|     file_hash, |     file_hash, | ||||||
|     deploy_to_k8s, |     deploy_to_k8s, | ||||||
|     publish_deployment, |     publish_deployment, | ||||||
|     get_requested_names, |     hostname_for_deployment_request, | ||||||
|     hostnames_for_deployment_request, |  | ||||||
|     generate_hostname_for_app, |     generate_hostname_for_app, | ||||||
|     match_owner, |     match_owner, | ||||||
|     skip_by_tag, |     skip_by_tag, | ||||||
| @ -49,7 +47,7 @@ from stack_orchestrator.deploy.webapp.util import ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def process_app_deployment_request( # noqa | def process_app_deployment_request( | ||||||
|     ctx, |     ctx, | ||||||
|     laconic: LaconicRegistryClient, |     laconic: LaconicRegistryClient, | ||||||
|     app_deployment_request, |     app_deployment_request, | ||||||
| @ -78,34 +76,25 @@ def process_app_deployment_request( # noqa | |||||||
|     logger.log(f"Retrieved app record {app_deployment_request.attributes.application}") |     logger.log(f"Retrieved app record {app_deployment_request.attributes.application}") | ||||||
| 
 | 
 | ||||||
|     # 2. determine dns |     # 2. determine dns | ||||||
|     requested_names = hostnames_for_deployment_request(app_deployment_request, laconic) |     requested_name = hostname_for_deployment_request(app_deployment_request, laconic) | ||||||
|     logger.log(f"Determined requested name(s): {','.join(str(x) for x in requested_names)}") |     logger.log(f"Determined requested name: {requested_name}") | ||||||
| 
 | 
 | ||||||
|     fqdns = [] |  | ||||||
|     for requested_name in requested_names: |  | ||||||
|     if "." in requested_name: |     if "." in requested_name: | ||||||
|         if "allow" == fqdn_policy or "preexisting" == fqdn_policy: |         if "allow" == fqdn_policy or "preexisting" == fqdn_policy: | ||||||
|                 fqdns.append(requested_name) |             fqdn = requested_name | ||||||
|         else: |         else: | ||||||
|             raise Exception( |             raise Exception( | ||||||
|                 f"{requested_name} is invalid: only unqualified hostnames are allowed." |                 f"{requested_name} is invalid: only unqualified hostnames are allowed." | ||||||
|             ) |             ) | ||||||
|     else: |     else: | ||||||
|             fqdns.append(f"{requested_name}.{default_dns_suffix}") |         fqdn = f"{requested_name}.{default_dns_suffix}" | ||||||
| 
 | 
 | ||||||
|     # Normalize case (just in case) |     # Normalize case (just in case) | ||||||
|     fqdns = [fqdn.lower() for fqdn in fqdns] |     fqdn = fqdn.lower() | ||||||
| 
 | 
 | ||||||
|     # 3. check ownership of existing dnsrecord(s) vs this request |     # 3. check ownership of existing dnsrecord vs this request | ||||||
|     dns_lrns = [] |  | ||||||
|     existing_dns_records_by_lrns = {} |  | ||||||
|     for fqdn in fqdns: |  | ||||||
|     dns_lrn = f"{dns_record_namespace}/{fqdn}" |     dns_lrn = f"{dns_record_namespace}/{fqdn}" | ||||||
|         dns_lrns.append(dns_lrn) |  | ||||||
| 
 |  | ||||||
|     dns_record = laconic.get_record(dns_lrn) |     dns_record = laconic.get_record(dns_lrn) | ||||||
|         existing_dns_records_by_lrns[dns_lrn] = dns_record |  | ||||||
| 
 |  | ||||||
|     if dns_record: |     if dns_record: | ||||||
|         matched_owner = match_owner(app_deployment_request, dns_record) |         matched_owner = match_owner(app_deployment_request, dns_record) | ||||||
|         if not matched_owner and dns_record.attributes.request: |         if not matched_owner and dns_record.attributes.request: | ||||||
| @ -115,7 +104,7 @@ def process_app_deployment_request( # noqa | |||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         if matched_owner: |         if matched_owner: | ||||||
|                 logger.log(f"Matched DnsRecord ownership for {fqdn}: {matched_owner}") |             logger.log(f"Matched DnsRecord ownership: {matched_owner}") | ||||||
|         else: |         else: | ||||||
|             raise Exception( |             raise Exception( | ||||||
|                 "Unable to confirm ownership of DnsRecord %s for request %s" |                 "Unable to confirm ownership of DnsRecord %s for request %s" | ||||||
| @ -156,49 +145,30 @@ def process_app_deployment_request( # noqa | |||||||
| 
 | 
 | ||||||
|     # 5. determine new or existing deployment |     # 5. determine new or existing deployment | ||||||
|     #   a. check for deployment lrn |     #   a. check for deployment lrn | ||||||
|     app_deployment_lrns = [f"{deployment_record_namespace}/{fqdn}" for fqdn in fqdns] |     app_deployment_lrn = f"{deployment_record_namespace}/{fqdn}" | ||||||
|     if app_deployment_request.attributes.deployment: |     if app_deployment_request.attributes.deployment: | ||||||
|         app_deployment_lrns = [app_deployment_request.attributes.deployment] |         app_deployment_lrn = app_deployment_request.attributes.deployment | ||||||
|         if not app_deployment_lrns[0].startswith(deployment_record_namespace): |     if not app_deployment_lrn.startswith(deployment_record_namespace): | ||||||
|         raise Exception( |         raise Exception( | ||||||
|             "Deployment LRN %s is not in a supported namespace" |             "Deployment LRN %s is not in a supported namespace" | ||||||
|             % app_deployment_request.attributes.deployment |             % app_deployment_request.attributes.deployment | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     # 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) |     deployment_record = laconic.get_record(app_deployment_lrn) | ||||||
|         if deployment_record: |     deployment_dir = os.path.join(deployment_parent_dir, fqdn) | ||||||
|             # 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 |     # 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 |     # TODO: implement support to derive this transparently from the already-unique deployment id | ||||||
|     unique_deployment_id = hashlib.md5(fqdns[-1].encode()).hexdigest()[:16] |     unique_deployment_id = hashlib.md5(fqdn.encode()).hexdigest()[:16] | ||||||
|  |     deployment_config_file = os.path.join(deployment_dir, "config.env") | ||||||
|     deployment_container_tag = "laconic-webapp/%s:local" % unique_deployment_id |     deployment_container_tag = "laconic-webapp/%s:local" % unique_deployment_id | ||||||
|     app_image_shared_tag = f"laconic-webapp/{app.id}:local" |     app_image_shared_tag = f"laconic-webapp/{app.id}:local" | ||||||
|     #   b. check for deployment directory (create if necessary) |     #   b. check for deployment directory (create if necessary) | ||||||
|     if not os.path.exists(existing_deployment_dir): |     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) | ||||||
|  |             ) | ||||||
|         logger.log( |         logger.log( | ||||||
|             f"Creating webapp deployment in: {deployment_dir} with container id: {deployment_container_tag}" |             f"Creating webapp deployment in: {deployment_dir} with container id: {deployment_container_tag}" | ||||||
|         ) |         ) | ||||||
| @ -206,23 +176,13 @@ def process_app_deployment_request( # noqa | |||||||
|             ctx, |             ctx, | ||||||
|             deployment_dir, |             deployment_dir, | ||||||
|             deployment_container_tag, |             deployment_container_tag, | ||||||
|             [f"https://{fqdn}" for fqdn in fqdns], |             f"https://{fqdn}", | ||||||
|             kube_config, |             kube_config, | ||||||
|             image_registry, |             image_registry, | ||||||
|             env_filename, |             env_filename, | ||||||
|         ) |         ) | ||||||
|     else: |     elif env_filename: | ||||||
|         # Rename deployment dir according to new request (last fqdn from given dns) |         shutil.copyfile(env_filename, deployment_config_file) | ||||||
|         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 |     needs_k8s_deploy = False | ||||||
|     if force_rebuild: |     if force_rebuild: | ||||||
| @ -272,11 +232,10 @@ def process_app_deployment_request( # noqa | |||||||
|     else: |     else: | ||||||
|         logger.log("Requested app is already deployed, skipping build and image push") |         logger.log("Requested app is already deployed, skipping build and image push") | ||||||
| 
 | 
 | ||||||
|     # 7. restart deployment on config or url spec change |     # 7. update config (if needed) | ||||||
|     if ( |     if ( | ||||||
|         not deployment_record |         not deployment_record | ||||||
|         or file_hash(os.path.join(deployment_dir, "config.env")) != deployment_record.attributes.meta.config |         or file_hash(deployment_config_file) != deployment_record.attributes.meta.config | ||||||
|         or len(fqdns_to_release) != 0 |  | ||||||
|     ): |     ): | ||||||
|         needs_k8s_deploy = True |         needs_k8s_deploy = True | ||||||
| 
 | 
 | ||||||
| @ -289,10 +248,9 @@ def process_app_deployment_request( # noqa | |||||||
|         laconic, |         laconic, | ||||||
|         app, |         app, | ||||||
|         deployment_record, |         deployment_record, | ||||||
|         app_deployment_lrns, |         app_deployment_lrn, | ||||||
|         existing_dns_records_by_lrns, |         dns_record, | ||||||
|         dns_lrns, |         dns_lrn, | ||||||
|         dns_record_namespace, |  | ||||||
|         deployment_dir, |         deployment_dir, | ||||||
|         dns_value, |         dns_value, | ||||||
|         app_deployment_request, |         app_deployment_request, | ||||||
| @ -300,17 +258,6 @@ def process_app_deployment_request( # noqa | |||||||
|         logger, |         logger, | ||||||
|     ) |     ) | ||||||
|     logger.log("Publication complete.") |     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") |     logger.log("END - process_app_deployment_request") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -570,15 +517,13 @@ def command(  # noqa: C901 | |||||||
|                     result = "ERROR" |                     result = "ERROR" | ||||||
|                     continue |                     continue | ||||||
| 
 | 
 | ||||||
|                 requested_names = get_requested_names(r) |                 requested_name = r.attributes.dns | ||||||
|                 if len(requested_names) == 0: |                 if not requested_name: | ||||||
|                     requested_names = [generate_hostname_for_app(app)] |                     requested_name = generate_hostname_for_app(app) | ||||||
|                     main_logger.log( |                     main_logger.log( | ||||||
|                         "Generating name %s for request %s." % (requested_names[0], r.id) |                         "Generating name %s for request %s." % (requested_name, r.id) | ||||||
|                     ) |                     ) | ||||||
| 
 | 
 | ||||||
|                 # Skip request if any of the names is superseded |  | ||||||
|                 for requested_name in requested_names: |  | ||||||
|                 if ( |                 if ( | ||||||
|                     requested_name in skipped_by_name |                     requested_name in skipped_by_name | ||||||
|                     or requested_name in requests_by_name |                     or requested_name in requests_by_name | ||||||
| @ -587,9 +532,6 @@ def command(  # noqa: C901 | |||||||
|                         "Ignoring request %s, it has been superseded." % r.id |                         "Ignoring request %s, it has been superseded." % r.id | ||||||
|                     ) |                     ) | ||||||
|                     result = "SKIP" |                     result = "SKIP" | ||||||
|                         break |  | ||||||
| 
 |  | ||||||
|                 if result == "SKIP": |  | ||||||
|                     continue |                     continue | ||||||
| 
 | 
 | ||||||
|                 if skip_by_tag(r, include_tags, exclude_tags): |                 if skip_by_tag(r, include_tags, exclude_tags): | ||||||
| @ -597,20 +539,15 @@ def command(  # noqa: C901 | |||||||
|                         "Skipping request %s, filtered by tag (include %s, exclude %s, present %s)" |                         "Skipping request %s, filtered by tag (include %s, exclude %s, present %s)" | ||||||
|                         % (r.id, include_tags, exclude_tags, r.attributes.tags) |                         % (r.id, include_tags, exclude_tags, r.attributes.tags) | ||||||
|                     ) |                     ) | ||||||
| 
 |  | ||||||
|                     for requested_name in requested_names: |  | ||||||
|                     skipped_by_name[requested_name] = r |                     skipped_by_name[requested_name] = r | ||||||
| 
 |  | ||||||
|                     result = "SKIP" |                     result = "SKIP" | ||||||
|                     continue |                     continue | ||||||
| 
 | 
 | ||||||
|                 main_logger.log( |                 main_logger.log( | ||||||
|                     "Found pending request %s to run application %s on %s." |                     "Found pending request %s to run application %s on %s." | ||||||
|                     % (r.id, r.attributes.application, ','.join(str(x) for x in requested_names)) |                     % (r.id, r.attributes.application, requested_name) | ||||||
|                 ) |                 ) | ||||||
| 
 |                 requests_by_name[requested_name] = r | ||||||
|                 # Set request for on of the names |  | ||||||
|                 requests_by_name[requested_names[-1]] = r |  | ||||||
|             except Exception as e: |             except Exception as e: | ||||||
|                 result = "ERROR" |                 result = "ERROR" | ||||||
|                 main_logger.log(f"ERROR examining request {r.id}: " + str(e)) |                 main_logger.log(f"ERROR examining request {r.id}: " + str(e)) | ||||||
|  | |||||||
| @ -76,7 +76,6 @@ def command(  # noqa: C901 | |||||||
|             "name": hostname, |             "name": hostname, | ||||||
|             "publicKey": pub_key, |             "publicKey": pub_key, | ||||||
|             "paymentAddress": payment_address, |             "paymentAddress": payment_address, | ||||||
|             "deployerVersion": "1.0.0", |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -21,7 +21,6 @@ import random | |||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
| import tempfile | import tempfile | ||||||
| from typing import List |  | ||||||
| import uuid | import uuid | ||||||
| import yaml | import yaml | ||||||
| 
 | 
 | ||||||
| @ -686,10 +685,9 @@ def publish_deployment( | |||||||
|     laconic: LaconicRegistryClient, |     laconic: LaconicRegistryClient, | ||||||
|     app_record, |     app_record, | ||||||
|     deploy_record, |     deploy_record, | ||||||
|     deployment_lrns, |     deployment_lrn, | ||||||
|     existing_dns_records_by_lrns, |     dns_record, | ||||||
|     dns_lrns: List[str], |     dns_lrn, | ||||||
|     dns_record_namespace, |  | ||||||
|     deployment_dir, |     deployment_dir, | ||||||
|     dns_value=None, |     dns_value=None, | ||||||
|     app_deployment_request=None, |     app_deployment_request=None, | ||||||
| @ -703,15 +701,14 @@ def publish_deployment( | |||||||
|             int(deploy_record.attributes.version.split(".")[-1]) + 1 |             int(deploy_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: |     if not dns_record: | ||||||
|         dns_ver = "0.0.1" |         dns_ver = "0.0.1" | ||||||
|     else: |     else: | ||||||
|         dns_ver = "0.0.%d" % (int(dns_record.attributes.version.split(".")[-1]) + 1) |         dns_ver = "0.0.%d" % (int(dns_record.attributes.version.split(".")[-1]) + 1) | ||||||
| 
 | 
 | ||||||
|         fqdn = dns_lrn.removeprefix(f"{dns_record_namespace}/") |     spec = yaml.full_load(open(os.path.join(deployment_dir, "spec.yml"))) | ||||||
|  |     fqdn = spec["network"]["http-proxy"][0]["host-name"] | ||||||
|  | 
 | ||||||
|     uniq = uuid.uuid4() |     uniq = uuid.uuid4() | ||||||
| 
 | 
 | ||||||
|     new_dns_record = { |     new_dns_record = { | ||||||
| @ -731,20 +728,15 @@ def publish_deployment( | |||||||
|     if logger: |     if logger: | ||||||
|         logger.log("Publishing DnsRecord.") |         logger.log("Publishing DnsRecord.") | ||||||
|     dns_id = laconic.publish(new_dns_record, [dns_lrn]) |     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"))) |  | ||||||
|     last_fqdn = spec["network"]["http-proxy"][-1]["host-name"] |  | ||||||
|     last_dns_id = dns_ids[-1] |  | ||||||
| 
 | 
 | ||||||
|     new_deployment_record = { |     new_deployment_record = { | ||||||
|         "record": { |         "record": { | ||||||
|             "type": "ApplicationDeploymentRecord", |             "type": "ApplicationDeploymentRecord", | ||||||
|             "version": deploy_ver, |             "version": deploy_ver, | ||||||
|             "url": f"https://{last_fqdn}", |             "url": f"https://{fqdn}", | ||||||
|             "name": app_record.attributes.name, |             "name": app_record.attributes.name, | ||||||
|             "application": app_record.id, |             "application": app_record.id, | ||||||
|             "dns": last_dns_id, |             "dns": dns_id, | ||||||
|             "meta": { |             "meta": { | ||||||
|                 "config": file_hash(os.path.join(deployment_dir, "config.env")), |                 "config": file_hash(os.path.join(deployment_dir, "config.env")), | ||||||
|                 "so": uniq.hex, |                 "so": uniq.hex, | ||||||
| @ -766,34 +758,21 @@ def publish_deployment( | |||||||
| 
 | 
 | ||||||
|     if logger: |     if logger: | ||||||
|         logger.log("Publishing ApplicationDeploymentRecord.") |         logger.log("Publishing ApplicationDeploymentRecord.") | ||||||
|     deployment_id = laconic.publish(new_deployment_record, deployment_lrns) |     deployment_id = laconic.publish(new_deployment_record, [deployment_lrn]) | ||||||
|     return {"dns": dns_ids, "deployment": deployment_id} |     return {"dns": dns_id, "deployment": deployment_id} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_requested_names(app_deployment_request): | def hostname_for_deployment_request(app_deployment_request, laconic): | ||||||
|     request_dns = app_deployment_request.attributes.dns |     dns_name = app_deployment_request.attributes.dns | ||||||
|     return request_dns.split(",") if request_dns else [] |     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 = laconic.get_record( | ||||||
|             app_deployment_request.attributes.application, require=True |             app_deployment_request.attributes.application, require=True | ||||||
|         ) |         ) | ||||||
|         return [generate_hostname_for_app(app)] |         dns_name = generate_hostname_for_app(app) | ||||||
| 
 |     elif dns_name.startswith("lrn://"): | ||||||
|     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) |         record = laconic.get_record(dns_name, require=True) | ||||||
|         dns_name = record.attributes.name |         dns_name = record.attributes.name | ||||||
| 
 |     return dns_name | ||||||
|         dns_names.append(dns_name) |  | ||||||
| 
 |  | ||||||
|     return dns_names |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def generate_hostname_for_app(app): | def generate_hostname_for_app(app): | ||||||
|  | |||||||
| @ -180,9 +180,7 @@ def get_k8s_dir(): | |||||||
| def get_parsed_deployment_spec(spec_file): | def get_parsed_deployment_spec(spec_file): | ||||||
|     spec_file_path = Path(spec_file) |     spec_file_path = Path(spec_file) | ||||||
|     try: |     try: | ||||||
|         with spec_file_path: |         return get_yaml().load(open(spec_file_path, "r")) | ||||||
|             deploy_spec = get_yaml().load(open(spec_file_path, "r")) |  | ||||||
|             return deploy_spec |  | ||||||
|     except FileNotFoundError as error: |     except FileNotFoundError as error: | ||||||
|         # We try here to generate a useful diagnostic error |         # We try here to generate a useful diagnostic error | ||||||
|         print(f"Error: spec file: {spec_file_path} does not exist") |         print(f"Error: spec file: {spec_file_path} does not exist") | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user