diff --git a/infra/deploy.py b/infra/deploy.py index 4ee83cf3cc0c948319a969aa6422fa2fc9902a7a..6fa15f25a25fa7b0ac4c534fd935bb2c0c3f0120 100755 --- a/infra/deploy.py +++ b/infra/deploy.py @@ -9,10 +9,12 @@ import requests import time import sys import os -from kubernetes import client, config +from kubernetes import client, config, utils from kubernetes.client.rest import ApiException import base64 from enum import Enum, auto +import subprocess +from jinja2 import Template, Environment, FileSystemLoader cwd = Path(os.path.dirname(os.path.realpath(__file__))) @@ -23,6 +25,33 @@ def deploy(): bootstrapCluster() +def apply(resource, message=None): + if len(resource.splitlines()) == 1: + argument = resource if "https://" in resource else (cwd / resource).as_posix() + kubectl = f"kubectl --kubeconfig={(cwd / 'kubeconfig.yaml').as_posix()} apply -f {argument}" + output, error = subprocess.Popen(kubectl.split(), stdout=subprocess.PIPE).communicate() + else: + kubectl = f"kubectl --kubeconfig={(cwd / 'kubeconfig.yaml').as_posix()} apply -f -" + result = subprocess.run( + kubectl.split(), + stdout=subprocess.PIPE, + input=resource, + encoding='ascii' + ) + output = result.stdout.encode() + error = result.stderr + + if error is None: + _logger.debug(f"{kubectl}\n{output.decode()}") + else: + if message is not None: + _logger.critical(f"{message} provision failed") + _logger.error(error) + raise Exception(f"kubectl failed : {kubectl}") + if message is not None: + _logger.info(message) + + def bootstrapCluster(): class SecretType(Enum): @@ -30,6 +59,9 @@ def bootstrapCluster(): BASICAUTH = auto() REGSITRY = auto() DASHBOARD = auto() + DO = auto() + STATUSSITE = auto() + SHEVASTREAM = auto() def createNamespace(namespace): try: @@ -41,19 +73,27 @@ def bootstrapCluster(): else: raise e - def createTlsSecret(namespace, type): + def createSecret(namespace, type): try: - if type == SecretType.TLS: + data = None + stringData = None + + if type == SecretType.TLS or type == SecretType.DASHBOARD: with open(_secretsPath / "certificate.key", "r") as key: with open(_secretsPath / "certificate.crt", "r") as certificate: - data = {'tls.crt': base64.b64encode(certificate.read().encode()).decode(), 'tls.key': base64.b64encode(key.read().encode()).decode()} - secType = 'kubernetes.io/tls' - metadata = {'name': 'lets-encrypt'} + if type == SecretType.TLS: + data = {"tls.crt": base64.b64encode(certificate.read().encode()).decode(), "tls.key": base64.b64encode(key.read().encode()).decode()} + secType = "kubernetes.io/tls" + metadata = {"name": "lets-encrypt"} + else: + data = {"certificate.crt": base64.b64encode(certificate.read().encode()).decode(), "certificate.key": base64.b64encode(key.read().encode()).decode()} + secType = "Opaque" + metadata = {"name": "kubernetes-dashboard-certs"} elif type == SecretType.BASICAUTH: with open(_secretsPath / "auth", "r") as auth: - data = {'auth': base64.b64encode(auth.read().encode()).decode()} - secType = 'Opaque' - metadata = {'name': 'basic-auth'} + data = {"auth": base64.b64encode(auth.read().encode()).decode()} + secType = "Opaque" + metadata = {"name": "basic-auth"} elif type == SecretType.REGSITRY: data = { ".dockerconfigjson": base64.b64encode( @@ -69,10 +109,24 @@ def bootstrapCluster(): }).encode() ).decode() } - secType = 'kubernetes.io/dockerconfigjson' - metadata = {'name': 'regsecret'} - - api.create_namespaced_secret(namespace, client.V1Secret(data=data, metadata=metadata, api_version="v1", type=secType)) + secType = "kubernetes.io/dockerconfigjson" + metadata = {"name": "regsecret"} + elif type == SecretType.DO: + stringData = {"access-token": _doToken} + secType = "Opaque" + metadata = {"name": "digitalocean"} + elif type == SecretType.STATUSSITE: + with open(_secretsPath / "appsettings.production.yml", "r") as config: + data = {"appsettings.production.yml": base64.b64encode(config.read().encode()).decode()} + secType = "Opaque" + metadata = {"name": "appsettings.production.yml"} + elif type == SecretType.SHEVASTREAM: + with open(cwd / "sources" / "shevastream" / "appsettings.json", "r") as config: + data = {"appsettings.json": base64.b64encode(config.read().encode()).decode()} + secType = "Opaque" + metadata = {"name": "shevastream-appsettings"} + + api.create_namespaced_secret(namespace, client.V1Secret(data=data, string_data=stringData, metadata=metadata, api_version="v1", type=secType)) _logger.info(f"Secret {type} added to {namespace}") except ApiException as e: if "already exists" in e.body: @@ -80,6 +134,12 @@ def bootstrapCluster(): else: raise e + def createDashboardIngress(): + templates = Environment(loader=FileSystemLoader(searchpath=str(cwd / "sources" / "dashboard"))) + config = templates.get_template("ingress.yaml").render(data={"token": "TODO"}) + print(config) + apply(config) + if not (cwd / "kubeconfig.yaml").is_file(): raise "Bootstrap called with no Kubeconfig file" @@ -89,9 +149,20 @@ def bootstrapCluster(): for namespace in namespaces: createNamespace(namespace) - createTlsSecret(namespace, SecretType.TLS) - createTlsSecret(namespace, SecretType.BASICAUTH) - createTlsSecret(namespace, SecretType.REGSITRY) + createSecret(namespace, SecretType.TLS) + createSecret(namespace, SecretType.BASICAUTH) + createSecret(namespace, SecretType.REGSITRY) + + createSecret("kube-system", SecretType.DASHBOARD) + createSecret("kube-system", SecretType.DO) + createSecret("status-site", SecretType.STATUSSITE) + createSecret("websites", SecretType.SHEVASTREAM) + + apply("sources/dashboard/all.yaml", "Dashboard") + apply("sources/nginx/mandatory.yaml", "NGINX ingress controller") + + apply("https://raw.githubusercontent.com/digitalocean/csi-digitalocean/master/deploy/kubernetes/releases/csi-digitalocean-v0.3.1.yaml", "DO Volume provisioner") + createDashboardIngress() def provisionCluster(): @@ -134,9 +205,6 @@ def generateServices(): def generateService(name, image, replicated=True, auth=False, rps=10): - import subprocess - from jinja2 import Template, Environment, FileSystemLoader - _logger.info(f"Generating {name} service") if name in domainUrlExceptions: @@ -168,7 +236,8 @@ def generateService(name, image, replicated=True, auth=False, rps=10): "replicated": replicated, "rps": rps, "auth": auth, - "hosts": hosts + "hosts": hosts, + "secret": None if "shevastream" not in name else ("appsettings", "/run/secrets/settings/", "shevastream-appsettings", "appsettings", "appsettings.production.json") } ) @@ -196,7 +265,7 @@ def kubeconfig(): if response.status_code == 200: try: - response = response.content.decode('utf-8') + response = response.content.decode("utf-8") _logger.debug(f"Kubeconfig \n{response}") with open(cwd / "kubeconfig.yaml", "w") as kubeconfigFile: kubeconfigFile.write(response) @@ -222,14 +291,14 @@ def setup_logger(level): datefmt=None, reset=True, log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red', + "DEBUG": "cyan", + "INFO": "green", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red", } ) - logger = logging.getLogger('example') + logger = logging.getLogger("example") handler = logging.StreamHandler() handler.setFormatter(formatter) logger.addHandler(handler) @@ -267,7 +336,7 @@ def main(): parser = argparse.ArgumentParser(description="Deploy K8S to Digital Ocean.") - parser.add_argument('--verbose', '-v', dest="verbose", action='count', default=4, help="Log level: CRITICAL, ERROR, WARNING, INFO, DEBUG") + parser.add_argument("--verbose", "-v", dest="verbose", action="count", default=4, help="Log level: CRITICAL, ERROR, WARNING, INFO, DEBUG") subparsers = parser.add_subparsers(title="commands", dest="command", required=True) @@ -287,6 +356,7 @@ def main(): bootstrapParser = subparsers.add_parser("bootstrap") addSecretsPathArgument(bootstrapParser) addDockerPassArgument(bootstrapParser) + addDOTokenArgument(bootstrapParser) args = parser.parse_args() @@ -297,10 +367,10 @@ def main(): global _secretsPath _logger = setup_logger(60 - args.verbose * 10) - _doToken = args.doToken if 'doToken' in args else "" - _name = args.name if 'name' in args else "" - _dockerPass = args.dockerPass if 'dockerPass' in args else "" - _secretsPath = args.secretsPath if 'secretsPath' in args else "" + _doToken = args.doToken if "doToken" in args else "" + _name = args.name if "name" in args else "" + _dockerPass = args.dockerPass if "dockerPass" in args else "" + _secretsPath = args.secretsPath if "secretsPath" in args else "" if args.command == "deploy": deploy() @@ -394,5 +464,5 @@ domainUrlExceptions = { namespaces = ["websites", "monitoring", "ingress", "status-site", "kube-system", "gitlab", "review"] -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/infra/sources/dashboard/ingress.yaml b/infra/sources/dashboard/ingress.yaml index 5569bf1a75a4b7b917d0a149be02f76a8813d9e0..e87dfbfba31ce974e2a19c630228f463f549d6ed 100644 --- a/infra/sources/dashboard/ingress.yaml +++ b/infra/sources/dashboard/ingress.yaml @@ -8,7 +8,7 @@ metadata: nginx.ingress.kubernetes.io/auth-realm: "Authentication Required!" nginx.ingress.kubernetes.io/auth-type: basic nginx.ingress.kubernetes.io/configuration-snippet: | - proxy_set_header Authorization "Bearer __DASHBOARD_TOKEN__"; + proxy_set_header Authorization "Bearer {{ data.token }}"; name: dashboard namespace: kube-system spec: diff --git a/infra/sources/service/website.yaml b/infra/sources/service/website.yaml index 72ea582bb8f2a5ce57ec1ea60344369979af2ce9..7375d131c15f7b912c7b051d340fb4c29a3a55a7 100644 --- a/infra/sources/service/website.yaml +++ b/infra/sources/service/website.yaml @@ -49,6 +49,19 @@ spec: - name: {{ data.name }} image: {{ data.image }} imagePullPolicy: Always + {% if data.secret is not none %} + volumeMounts: + - name: {{ data.secret[0] }} + mountPath: {{ data.secret[1] }} + volumes: + - name: {{ data.secret[0] }} + secret: + secretName: {{ data.secret[2] }} + items: + - key: {{ data.secret[3] }} + path: {{ data.secret[4] }} + {% endif %} + {% else %}