forked from cerc-io/stack-orchestrator
Auto-detect which certificate to use (including wildcards). (#779)
Rather than always requesting a certificate, attempt to re-use an existing certificate if it already exists in the k8s cluster. This includes matching to a wildcard certificate. Reviewed-on: cerc-io/stack-orchestrator#779 Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com> Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
This commit is contained in:
parent
62f7ce649d
commit
523b5779be
@ -101,7 +101,7 @@ class ClusterInfo:
|
|||||||
)
|
)
|
||||||
return service
|
return service
|
||||||
|
|
||||||
def get_ingress(self, use_tls=False):
|
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()
|
||||||
ingress = None
|
ingress = None
|
||||||
@ -114,8 +114,8 @@ class ClusterInfo:
|
|||||||
host_name = http_proxy_info["host-name"]
|
host_name = http_proxy_info["host-name"]
|
||||||
rules = []
|
rules = []
|
||||||
tls = [client.V1IngressTLS(
|
tls = [client.V1IngressTLS(
|
||||||
hosts=[host_name],
|
hosts=certificate["spec"]["dnsNames"] if certificate else [host_name],
|
||||||
secret_name=f"{self.app_name}-tls"
|
secret_name=certificate["spec"]["secretName"] if certificate else f"{self.app_name}-tls"
|
||||||
)] if use_tls else None
|
)] if use_tls else None
|
||||||
paths = []
|
paths = []
|
||||||
for route in http_proxy_info["routes"]:
|
for route in http_proxy_info["routes"]:
|
||||||
@ -147,13 +147,17 @@ class ClusterInfo:
|
|||||||
tls=tls,
|
tls=tls,
|
||||||
rules=rules
|
rules=rules
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ingress_annotations = {
|
||||||
|
"kubernetes.io/ingress.class": "nginx",
|
||||||
|
}
|
||||||
|
if not certificate:
|
||||||
|
ingress_annotations["cert-manager.io/cluster-issuer"] = cluster_issuer
|
||||||
|
|
||||||
ingress = client.V1Ingress(
|
ingress = client.V1Ingress(
|
||||||
metadata=client.V1ObjectMeta(
|
metadata=client.V1ObjectMeta(
|
||||||
name=f"{self.app_name}-ingress",
|
name=f"{self.app_name}-ingress",
|
||||||
annotations={
|
annotations=ingress_annotations
|
||||||
"kubernetes.io/ingress.class": "nginx",
|
|
||||||
"cert-manager.io/cluster-issuer": "letsencrypt-prod"
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
spec=spec
|
spec=spec
|
||||||
)
|
)
|
||||||
|
@ -169,6 +169,39 @@ class K8sDeployer(Deployer):
|
|||||||
print("Service created:")
|
print("Service created:")
|
||||||
print(f"{service_resp}")
|
print(f"{service_resp}")
|
||||||
|
|
||||||
|
def _find_certificate_for_host_name(self, host_name):
|
||||||
|
all_certificates = self.custom_obj_api.list_namespaced_custom_object(
|
||||||
|
group="cert-manager.io",
|
||||||
|
version="v1",
|
||||||
|
namespace=self.k8s_namespace,
|
||||||
|
plural="certificates"
|
||||||
|
)
|
||||||
|
|
||||||
|
host_parts = host_name.split(".", 1)
|
||||||
|
host_as_wild = None
|
||||||
|
if len(host_parts) == 2:
|
||||||
|
host_as_wild = f"*.{host_parts[1]}"
|
||||||
|
|
||||||
|
now = datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||||
|
fmt = "%Y-%m-%dT%H:%M:%S%z"
|
||||||
|
|
||||||
|
# Walk over all the configured certificates.
|
||||||
|
for cert in all_certificates["items"]:
|
||||||
|
dns = cert["spec"]["dnsNames"]
|
||||||
|
# Check for an exact hostname match or a wildcard match.
|
||||||
|
if host_name in dns or host_as_wild in dns:
|
||||||
|
status = cert.get("status", {})
|
||||||
|
# Check the certificate date.
|
||||||
|
if "notAfter" in status and "notBefore" in status:
|
||||||
|
before = datetime.strptime(status["notBefore"], fmt)
|
||||||
|
after = datetime.strptime(status["notAfter"], fmt)
|
||||||
|
if before < now < after:
|
||||||
|
# Check the status is Ready
|
||||||
|
for condition in status.get("conditions", []):
|
||||||
|
if "True" == condition.get("status") and "Ready" == condition.get("type"):
|
||||||
|
return cert
|
||||||
|
return None
|
||||||
|
|
||||||
def up(self, detach, services):
|
def up(self, detach, services):
|
||||||
if not opts.o.dry_run:
|
if not opts.o.dry_run:
|
||||||
if self.is_kind():
|
if self.is_kind():
|
||||||
@ -189,8 +222,15 @@ class K8sDeployer(Deployer):
|
|||||||
self._create_volume_data()
|
self._create_volume_data()
|
||||||
self._create_deployment()
|
self._create_deployment()
|
||||||
|
|
||||||
|
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)
|
||||||
ingress: client.V1Ingress = self.cluster_info.get_ingress(use_tls=not self.is_kind())
|
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}")
|
||||||
|
|
||||||
|
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}")
|
||||||
@ -350,9 +390,11 @@ class K8sDeployer(Deployer):
|
|||||||
name=ingress.spec.tls[0].secret_name
|
name=ingress.spec.tls[0].secret_name
|
||||||
)
|
)
|
||||||
|
|
||||||
hostname = ingress.spec.tls[0].hosts[0]
|
hostname = ingress.spec.rules[0].host
|
||||||
ip = ingress.status.load_balancer.ingress[0].ip
|
ip = ingress.status.load_balancer.ingress[0].ip
|
||||||
tls = "notBefore: %s, notAfter: %s" % (cert["status"]["notBefore"], cert["status"]["notAfter"])
|
tls = "notBefore: %s; notAfter: %s; names: %s" % (
|
||||||
|
cert["status"]["notBefore"], cert["status"]["notAfter"], ingress.spec.tls[0].hosts
|
||||||
|
)
|
||||||
except: # noqa: E722
|
except: # noqa: E722
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -27,7 +27,9 @@ class ResourceLimits:
|
|||||||
memory: int = None
|
memory: int = None
|
||||||
storage: int = None
|
storage: int = None
|
||||||
|
|
||||||
def __init__(self, obj={}):
|
def __init__(self, obj=None):
|
||||||
|
if obj is None:
|
||||||
|
obj = {}
|
||||||
if "cpus" in obj:
|
if "cpus" in obj:
|
||||||
self.cpus = float(obj["cpus"])
|
self.cpus = float(obj["cpus"])
|
||||||
if "memory" in obj:
|
if "memory" in obj:
|
||||||
@ -50,7 +52,9 @@ class Resources:
|
|||||||
limits: ResourceLimits = None
|
limits: ResourceLimits = None
|
||||||
reservations: ResourceLimits = None
|
reservations: ResourceLimits = None
|
||||||
|
|
||||||
def __init__(self, obj={}):
|
def __init__(self, obj=None):
|
||||||
|
if obj is None:
|
||||||
|
obj = {}
|
||||||
if "reservations" in obj:
|
if "reservations" in obj:
|
||||||
self.reservations = ResourceLimits(obj["reservations"])
|
self.reservations = ResourceLimits(obj["reservations"])
|
||||||
if "limits" in obj:
|
if "limits" in obj:
|
||||||
@ -72,7 +76,9 @@ class Spec:
|
|||||||
obj: typing.Any
|
obj: typing.Any
|
||||||
file_path: Path
|
file_path: Path
|
||||||
|
|
||||||
def __init__(self, file_path: Path = None, obj={}) -> None:
|
def __init__(self, file_path: Path = None, obj=None) -> None:
|
||||||
|
if obj is None:
|
||||||
|
obj = {}
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
|
|
||||||
@ -91,49 +97,41 @@ class Spec:
|
|||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
|
|
||||||
def get_image_registry(self):
|
def get_image_registry(self):
|
||||||
return (self.obj[constants.image_registry_key]
|
return self.obj.get(constants.image_registry_key)
|
||||||
if self.obj and constants.image_registry_key in self.obj
|
|
||||||
else None)
|
|
||||||
|
|
||||||
def get_volumes(self):
|
def get_volumes(self):
|
||||||
return (self.obj["volumes"]
|
return self.obj.get(constants.volumes_key, {})
|
||||||
if self.obj and "volumes" in self.obj
|
|
||||||
else {})
|
|
||||||
|
|
||||||
def get_configmaps(self):
|
def get_configmaps(self):
|
||||||
return (self.obj["configmaps"]
|
return self.obj.get(constants.configmaps_key, {})
|
||||||
if self.obj and "configmaps" in self.obj
|
|
||||||
else {})
|
|
||||||
|
|
||||||
def get_container_resources(self):
|
def get_container_resources(self):
|
||||||
return Resources(self.obj.get("resources", {}).get("containers", {}))
|
return Resources(self.obj.get(constants.resources_key, {}).get("containers", {}))
|
||||||
|
|
||||||
def get_volume_resources(self):
|
def get_volume_resources(self):
|
||||||
return Resources(self.obj.get("resources", {}).get("volumes", {}))
|
return Resources(self.obj.get(constants.resources_key, {}).get(constants.volumes_key, {}))
|
||||||
|
|
||||||
def get_http_proxy(self):
|
def get_http_proxy(self):
|
||||||
return (self.obj[constants.network_key][constants.http_proxy_key]
|
return self.obj.get(constants.network_key, {}).get(constants.http_proxy_key, [])
|
||||||
if self.obj and constants.network_key in self.obj
|
|
||||||
and constants.http_proxy_key in self.obj[constants.network_key]
|
|
||||||
else None)
|
|
||||||
|
|
||||||
def get_annotations(self):
|
def get_annotations(self):
|
||||||
return self.obj.get("annotations", {})
|
return self.obj.get(constants.annotations_key, {})
|
||||||
|
|
||||||
def get_labels(self):
|
def get_labels(self):
|
||||||
return self.obj.get("labels", {})
|
return self.obj.get(constants.labels_key, {})
|
||||||
|
|
||||||
def get_privileged(self):
|
def get_privileged(self):
|
||||||
return "true" == str(self.obj.get("security", {}).get("privileged", "false")).lower()
|
return "true" == str(self.obj.get(constants.security_key, {}).get("privileged", "false")).lower()
|
||||||
|
|
||||||
def get_capabilities(self):
|
def get_capabilities(self):
|
||||||
return self.obj.get("security", {}).get("capabilities", [])
|
return self.obj.get(constants.security_key, {}).get("capabilities", [])
|
||||||
|
|
||||||
def get_deployment_type(self):
|
def get_deployment_type(self):
|
||||||
return self.obj[constants.deploy_to_key]
|
return self.obj.get(constants.deploy_to_key)
|
||||||
|
|
||||||
def is_kubernetes_deployment(self):
|
def is_kubernetes_deployment(self):
|
||||||
return self.get_deployment_type() in [constants.k8s_kind_deploy_type, constants.k8s_deploy_type]
|
return self.get_deployment_type() in [constants.k8s_kind_deploy_type,
|
||||||
|
constants.k8s_deploy_type]
|
||||||
|
|
||||||
def is_kind_deployment(self):
|
def is_kind_deployment(self):
|
||||||
return self.get_deployment_type() in [constants.k8s_kind_deploy_type]
|
return self.get_deployment_type() in [constants.k8s_kind_deploy_type]
|
||||||
|
Loading…
Reference in New Issue
Block a user