diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp.py b/stack_orchestrator/deploy/webapp/deploy_webapp.py index 4c91dec3..375814f7 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp.py @@ -15,6 +15,7 @@ import click import os +import sys from pathlib import Path from urllib.parse import urlparse from tempfile import NamedTemporaryFile @@ -23,6 +24,7 @@ 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 from stack_orchestrator.deploy.deploy_types import DeployCommandContext +from stack_orchestrator.deploy.webapp.util import TlsDetails def _fixup_container_tag(deployment_dir: str, image: str): @@ -36,16 +38,16 @@ def _fixup_container_tag(deployment_dir: str, image: str): wfile.write(contents) -def _fixup_url_spec(spec_file_name: str, url: str): +def _fixup_url_spec(spec_file_name: str, url: str, tls_details: TlsDetails = TlsDetails()): # url is like: https://example.com/path parsed_url = urlparse(url) - http_proxy_spec = f''' - http-proxy: + 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 - ''' +{tls_details.to_yaml(indent=6)} +''' spec_file_path = Path(spec_file_name) with open(spec_file_path) as rfile: contents = rfile.read() @@ -54,7 +56,8 @@ def _fixup_url_spec(spec_file_name: str, url: str): wfile.write(contents) -def create_deployment(ctx, deployment_dir, image, url, kube_config, image_registry, env_file): +def create_deployment(ctx, deployment_dir, image, url, kube_config, image_registry, env_file, + tls_details: TlsDetails = None): # 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 +89,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, url, tls_details) create_operation( deploy_command_context, spec_file_name, @@ -116,8 +119,16 @@ def command(ctx): @click.option("--image", help="image to deploy", required=True) @click.option("--url", help="url to serve", required=True) @click.option("--env-file", help="environment file for webapp") +@click.option("--tls-host", help="Override TLS hostname (eg, '*.mydomain.com')") +@click.option("--tls-secret", help="Override TLS secret name") +@click.option("--tls-issuer", help="TLS issuer to use (default: letsencrypt-prod)") @click.pass_context -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, tls_host, tls_secret, tls_issuer): '''create a deployment for the specified webapp container''' - return create_deployment(ctx, deployment_dir, image, url, kube_config, image_registry, env_file) + if (tls_secret and not tls_host) or (tls_host and not tls_secret): + print("Cannot specify --tls-host without --tls-secret", file=sys.stderr) + sys.exit(2) + + return create_deployment(ctx, deployment_dir, image, url, kube_config, image_registry, env_file, + TlsDetails(tls_host, tls_secret, tls_issuer)) diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index 2cc704ff..caf2e8be 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -26,7 +26,7 @@ import click from stack_orchestrator.deploy.images import remote_image_exists, add_tags_to_image from stack_orchestrator.deploy.webapp import deploy_webapp -from stack_orchestrator.deploy.webapp.util import (LaconicRegistryClient, TimedLogger, +from stack_orchestrator.deploy.webapp.util import (LaconicRegistryClient, TimedLogger, TlsDetails, build_container_image, push_container_image, file_hash, deploy_to_k8s, publish_deployment, hostname_for_deployment_request, generate_hostname_for_app, @@ -44,7 +44,8 @@ def process_app_deployment_request( kube_config, image_registry, force_rebuild, - logger + tls_details, + logger, ): logger.log("BEGIN - process_app_deployment_request") @@ -198,11 +199,14 @@ def dump_known_requests(filename, requests, status="SEEN"): @click.option("--exclude-tags", help="Exclude requests with matching tags (comma-separated).", default="") @click.option("--force-rebuild", help="Rebuild even if the image already exists.", is_flag=True) @click.option("--log-dir", help="Output build/deployment logs to directory.", default=None) +@click.option("--tls-host", help="Override TLS hostname (eg, '*.mydomain.com')") +@click.option("--tls-secret", help="Override TLS secret name") +@click.option("--tls-issuer", help="TLS issuer to use (default: letsencrypt-prod)") @click.pass_context def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_dir, # noqa: C901 request_id, discover, state_file, only_update_state, dns_suffix, record_namespace_dns, record_namespace_deployments, dry_run, - include_tags, exclude_tags, force_rebuild, log_dir): + include_tags, exclude_tags, force_rebuild, log_dir, tls_host, tls_secret, tls_issuer): if request_id and discover: print("Cannot specify both --request-id and --discover", file=sys.stderr) sys.exit(2) @@ -220,6 +224,10 @@ def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_ print("--dns-suffix, --record-namespace-dns, and --record-namespace-deployments are all required", file=sys.stderr) sys.exit(2) + if (tls_secret and not tls_host) or (tls_host and not tls_secret): + print("Cannot specify --tls-host without --tls-secret", file=sys.stderr) + sys.exit(2) + # Split CSV and clean up values. include_tags = [tag.strip() for tag in include_tags.split(",") if tag] exclude_tags = [tag.strip() for tag in exclude_tags.split(",") if tag] @@ -305,6 +313,7 @@ def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_ print("Found %d unsatisfied request(s) to process." % len(requests_to_execute)) if not dry_run: + tls_details = TlsDetails(tls_host, tls_secret, tls_issuer) for r in requests_to_execute: dump_known_requests(state_file, [r], "DEPLOYING") status = "ERROR" @@ -334,6 +343,7 @@ def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_ kube_config, image_registry, force_rebuild, + tls_details, logger ) status = "DEPLOYED" diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index 5c484ed1..3440832e 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -26,6 +26,40 @@ import uuid import yaml +class TlsDetails: + def __init__(self, host_or_hosts=None, secret_name: str = None, issuer_name: str = None): + if host_or_hosts: + if isinstance(host_or_hosts, list): + self.hosts = host_or_hosts + else: + self.hosts = [host_or_hosts] + else: + self.hosts = None + self.secret_name = secret_name + self.issuer_name = issuer_name + + def to_yaml(self, indent=6): + if not self.hosts and not self.secret_name and not self.issuer_name: + return "" + + ret = " " * indent + "tls:\n" + indent += 2 + + if self.issuer_name: + ret += " " * indent + "issuer: '%s'\n" % self.issuer_name + + if self.secret_name: + ret += " " * indent + "secret: '%s'\n" % self.secret_name + + if self.hosts: + ret += " " * indent + "hosts:" + indent += 2 + for h in self.hosts: + ret += "\n" + " " * indent + "- '%s'" % h + + return ret + + class AttrDict(dict): def __init__(self, *args, **kwargs): super(AttrDict, self).__init__(*args, **kwargs)