Compare commits

..

No commits in common. "pm-multiple-domain-deployments" and "main" have entirely different histories.

6 changed files with 185 additions and 319 deletions

View File

@ -114,27 +114,22 @@ class ClusterInfo:
nodeports.append(service)
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
http_proxy_info_list = self.spec.get_http_proxy()
if not http_proxy_info_list:
return None
tls = [] if use_tls else None
rules = []
for http_proxy_info in http_proxy_info_list:
ingress = None
if http_proxy_info_list:
# TODO: handle multiple definitions
http_proxy_info = http_proxy_info_list[0]
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"]
certificate = certificate_by_host[host_name] if host_name in certificate_by_host else None
if use_tls:
tls.append(client.V1IngressTLS(
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}-{host_name}-tls"
))
secret_name=certificate["spec"]["secretName"] if certificate else f"{self.app_name}-tls"
)] if use_tls else None
paths = []
for route in http_proxy_info["routes"]:
path = route["path"]

View File

@ -227,18 +227,15 @@ class K8sDeployer(Deployer):
self._create_volume_data()
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)
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:
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}")
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 opts.o.debug:
print(f"Sending this ingress: {ingress}")
@ -384,46 +381,36 @@ class K8sDeployer(Deployer):
if not pods:
return
tls_by_host = {}
hostname = "?"
ip = "?"
tls = "?"
try:
ingress = self.networking_api.read_namespaced_ingress(namespace=self.k8s_namespace,
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(
group="cert-manager.io",
version="v1",
namespace=self.k8s_namespace,
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" % (
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
pass
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("\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})")

View File

@ -13,16 +13,12 @@
# 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/>.
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
@ -40,38 +36,25 @@ def _fixup_container_tag(deployment_dir: str, image: str):
wfile.write(contents)
def _fixup_url_spec(spec_file_name: str, urls: List[str]):
spec_file_path = Path(spec_file_name)
# 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:
def _fixup_url_spec(spec_file_name: str, url: str):
# url is like: https://example.com/path
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)
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
'''
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)
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:
# 1. laconic-so --stack webapp-template deploy --deploy-to k8s init --output webapp-spec.yml
# --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
)
# Add the TLS and DNS spec
_fixup_url_spec(spec_file_name, urls)
_fixup_url_spec(spec_file_name, url)
create_operation(
deploy_command_context,
spec_file_name,
@ -116,20 +99,6 @@ def create_deployment(ctx, deployment_dir, image, urls, kube_config, image_regis
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):
@ -151,4 +120,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)

View File

@ -20,7 +20,6 @@ import shutil
import sys
import tempfile
import time
from typing import List
import uuid
import yaml
@ -39,8 +38,7 @@ from stack_orchestrator.deploy.webapp.util import (
file_hash,
deploy_to_k8s,
publish_deployment,
get_requested_names,
hostnames_for_deployment_request,
hostname_for_deployment_request,
generate_hostname_for_app,
match_owner,
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,
laconic: LaconicRegistryClient,
app_deployment_request,
@ -78,34 +76,25 @@ def process_app_deployment_request( # noqa
logger.log(f"Retrieved app record {app_deployment_request.attributes.application}")
# 2. determine dns
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)}")
requested_name = hostname_for_deployment_request(app_deployment_request, laconic)
logger.log(f"Determined requested name: {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)
fqdn = requested_name
else:
raise Exception(
f"{requested_name} is invalid: only unqualified hostnames are allowed."
)
else:
fqdns.append(f"{requested_name}.{default_dns_suffix}")
fqdn = f"{requested_name}.{default_dns_suffix}"
# 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
dns_lrns = []
existing_dns_records_by_lrns = {}
for fqdn in fqdns:
# 3. check ownership of existing dnsrecord vs this request
dns_lrn = f"{dns_record_namespace}/{fqdn}"
dns_lrns.append(dns_lrn)
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:
@ -115,7 +104,7 @@ def process_app_deployment_request( # noqa
)
if matched_owner:
logger.log(f"Matched DnsRecord ownership for {fqdn}: {matched_owner}")
logger.log(f"Matched DnsRecord ownership: {matched_owner}")
else:
raise Exception(
"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
# 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:
app_deployment_lrns = [app_deployment_request.attributes.deployment]
if not app_deployment_lrns[0].startswith(deployment_record_namespace):
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
)
# 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
deployment_dir = os.path.join(deployment_parent_dir, fqdn)
# 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(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
app_image_shared_tag = f"laconic-webapp/{app.id}:local"
# 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(
f"Creating webapp deployment in: {deployment_dir} with container id: {deployment_container_tag}"
)
@ -206,23 +176,13 @@ def process_app_deployment_request( # noqa
ctx,
deployment_dir,
deployment_container_tag,
[f"https://{fqdn}" for fqdn in fqdns],
f"https://{fqdn}",
kube_config,
image_registry,
env_filename,
)
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)
elif env_filename:
shutil.copyfile(env_filename, deployment_config_file)
needs_k8s_deploy = False
if force_rebuild:
@ -272,11 +232,10 @@ def process_app_deployment_request( # noqa
else:
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 (
not deployment_record
or file_hash(os.path.join(deployment_dir, "config.env")) != deployment_record.attributes.meta.config
or len(fqdns_to_release) != 0
or file_hash(deployment_config_file) != deployment_record.attributes.meta.config
):
needs_k8s_deploy = True
@ -289,10 +248,9 @@ def process_app_deployment_request( # noqa
laconic,
app,
deployment_record,
app_deployment_lrns,
existing_dns_records_by_lrns,
dns_lrns,
dns_record_namespace,
app_deployment_lrn,
dns_record,
dns_lrn,
deployment_dir,
dns_value,
app_deployment_request,
@ -300,17 +258,6 @@ def process_app_deployment_request( # noqa
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")
@ -570,15 +517,13 @@ def command( # noqa: C901
result = "ERROR"
continue
requested_names = get_requested_names(r)
if len(requested_names) == 0:
requested_names = [generate_hostname_for_app(app)]
requested_name = r.attributes.dns
if not requested_name:
requested_name = generate_hostname_for_app(app)
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 (
requested_name in skipped_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
)
result = "SKIP"
break
if result == "SKIP":
continue
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)"
% (r.id, include_tags, exclude_tags, r.attributes.tags)
)
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, ','.join(str(x) for x in requested_names))
% (r.id, r.attributes.application, requested_name)
)
# Set request for on of the names
requests_by_name[requested_names[-1]] = r
requests_by_name[requested_name] = r
except Exception as e:
result = "ERROR"
main_logger.log(f"ERROR examining request {r.id}: " + str(e))

View File

@ -76,7 +76,6 @@ def command( # noqa: C901
"name": hostname,
"publicKey": pub_key,
"paymentAddress": payment_address,
"deployerVersion": "1.0.0",
}
}

View File

@ -21,7 +21,6 @@ import random
import subprocess
import sys
import tempfile
from typing import List
import uuid
import yaml
@ -686,10 +685,9 @@ def publish_deployment(
laconic: LaconicRegistryClient,
app_record,
deploy_record,
deployment_lrns,
existing_dns_records_by_lrns,
dns_lrns: List[str],
dns_record_namespace,
deployment_lrn,
dns_record,
dns_lrn,
deployment_dir,
dns_value=None,
app_deployment_request=None,
@ -703,15 +701,14 @@ def publish_deployment(
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:
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}/")
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 = {
@ -731,20 +728,15 @@ def publish_deployment(
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")))
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://{last_fqdn}",
"url": f"https://{fqdn}",
"name": app_record.attributes.name,
"application": app_record.id,
"dns": last_dns_id,
"dns": dns_id,
"meta": {
"config": file_hash(os.path.join(deployment_dir, "config.env")),
"so": uniq.hex,
@ -766,34 +758,21 @@ def publish_deployment(
if logger:
logger.log("Publishing ApplicationDeploymentRecord.")
deployment_id = laconic.publish(new_deployment_record, deployment_lrns)
return {"dns": dns_ids, "deployment": deployment_id}
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 hostnames_for_deployment_request(app_deployment_request, laconic):
requested_names = get_requested_names(app_deployment_request)
if len(requested_names) == 0:
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
)
return [generate_hostname_for_app(app)]
dns_names = []
for requested_name in requested_names:
dns_name = requested_name
if dns_name.startswith("lrn://"):
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
dns_names.append(dns_name)
return dns_names
return dns_name
def generate_hostname_for_app(app):