From 726a5875eb0486a50f98971fa62789cff7b217b0 Mon Sep 17 00:00:00 2001 From: Kris Nova Date: Fri, 24 Mar 2017 11:59:22 -0600 Subject: [PATCH] feat(ingress): Feature work for experimental native ingress Adding ingress support to controller Adding changes to chart to support feature Adding ingress rules for controller API, and SSH on TCP 2222 Adding ingress support in general Requires deis/workflow#732 Requires deis/builder#495 Requires deis/router#316 Technically a non breaking change, as the user must opt-in to the feature --- .../templates/controller-deployment.yaml | 5 ++ .../controller-ingress-rule-http-80.yaml | 21 ++++++ charts/controller/values.yaml | 10 +++ rootfs/api/models/app.py | 14 +++- rootfs/api/settings/production.py | 4 ++ rootfs/scheduler/resources/ingress.py | 66 +++++++++++++++++++ 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 charts/controller/templates/controller-ingress-rule-http-80.yaml create mode 100644 rootfs/scheduler/resources/ingress.py diff --git a/charts/controller/templates/controller-deployment.yaml b/charts/controller/templates/controller-deployment.yaml index 3156f0575..82732ed57 100644 --- a/charts/controller/templates/controller-deployment.yaml +++ b/charts/controller/templates/controller-deployment.yaml @@ -58,6 +58,11 @@ spec: # NOTE(bacongobbler): use deis/registry_proxy to work around Docker --insecure-registry requirements - name: "DEIS_REGISTRY_SERVICE_HOST" value: "127.0.0.1" + # Environmental variable value for $EXPERIMENTAL_NATIVE_INGRESS + - name: "EXPERIMENTAL_NATIVE_INGRESS" + value: "{{ .Values.global.experimental_native_ingress }}" + - name: "EXPERIMENTAL_NATIVE_INGRESS_HOSTNAME" + value: "{{ .Values.platform_domain }}" - name: "K8S_API_VERIFY_TLS" value: "{{ .Values.k8s_api_verify_tls }}" - name: "DEIS_REGISTRY_SERVICE_PORT" diff --git a/charts/controller/templates/controller-ingress-rule-http-80.yaml b/charts/controller/templates/controller-ingress-rule-http-80.yaml new file mode 100644 index 000000000..2d9cd2004 --- /dev/null +++ b/charts/controller/templates/controller-ingress-rule-http-80.yaml @@ -0,0 +1,21 @@ +{{ if .Values.global.experimental_native_ingress }} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + namespace: "deis" + name: "controller-api-server-ingress-http" + labels: + app: "controller" + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + rules: + - host: deis.{{ .Values.platform_domain }} + http: + paths: + - path: / + backend: + serviceName: deis-controller + servicePort: 80 +{{- end }} diff --git a/charts/controller/values.yaml b/charts/controller/values.yaml index 21c0118fa..7f33cddbc 100644 --- a/charts/controller/values.yaml +++ b/charts/controller/values.yaml @@ -14,6 +14,10 @@ deploy_hook_urls: "" registration_mode: "admin_only" # Option to disable ssl verification to connect to k8s api server k8s_api_verify_tls: "true" +# The public resolvable hostname to build your cluster with. +# +# This will be the hostname that is used to build endpoints such as "deis.$HOSTNAME" +platform_domain: "" global: # Set the storage backend @@ -45,3 +49,9 @@ global: host_port: 5555 # Prefix for the imagepull secret created when using private registry secret_prefix: "private-registry" + # Experimental feature to toggle using kubernetes ingress instead of the Deis router. + # + # Valid values are: + # - true: The deis controller will now create Kubernetes ingress rules for each app, and ingress rules will automatically be created for the controller itself. + # - false: The default mode, and the default behavior of Deis workflow. + experimental_native_ingress: false diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py index edf748939..7781eef12 100644 --- a/rootfs/api/models/app.py +++ b/rootfs/api/models/app.py @@ -191,6 +191,7 @@ def create(self, *args, **kwargs): # noqa # create required minimum resources in k8s for the application namespace = self.id + ingress = self.id service = self.id quota_name = '{}-quota'.format(self.id) try: @@ -200,7 +201,6 @@ def create(self, *args, **kwargs): # noqa self._scheduler.ns.get(namespace) except KubeException: self._scheduler.ns.create(namespace) - if settings.KUBERNETES_NAMESPACE_DEFAULT_QUOTA_SPEC != '': quota_spec = json.loads(settings.KUBERNETES_NAMESPACE_DEFAULT_QUOTA_SPEC) self.log('creating Quota {} for namespace {}'.format(quota_name, namespace), @@ -224,6 +224,18 @@ def create(self, *args, **kwargs): # noqa raise ServiceUnavailable('Kubernetes resources could not be created') from e + try: + # In order to create an ingress, we must first have a namespace. + if settings.EXPERIMENTAL_NATIVE_INGRESS: + try: + self._scheduler.ingress.get(ingress) + except KubeException: + self.log("creating Ingress {}".format(namespace), level=logging.INFO) + self._scheduler.ingress.create(ingress, + namespace, + settings.EXPERIMENTAL_NATIVE_INGRESS_HOSTNAME) + except KubeException as e: + raise ServiceUnavailable('Could not create Ingress in Kubernetes') from e try: self.appsettings_set.latest() except AppSettings.DoesNotExist: diff --git a/rootfs/api/settings/production.py b/rootfs/api/settings/production.py index 2fa833e1f..b23dfdb13 100644 --- a/rootfs/api/settings/production.py +++ b/rootfs/api/settings/production.py @@ -260,6 +260,10 @@ SECRET_KEY = os.environ.get('DEIS_SECRET_KEY', random_secret) BUILDER_KEY = os.environ.get('DEIS_BUILDER_KEY', random_secret) +# experimental native ingress +EXPERIMENTAL_NATIVE_INGRESS = bool(os.environ.get('EXPERIMENTAL_NATIVE_INGRESS', 0)) +EXPERIMENTAL_NATIVE_INGRESS_HOSTNAME = os.environ.get('EXPERIMENTAL_NATIVE_INGRESS_HOSTNAME', '') + # k8s image policies SLUGRUNNER_IMAGE = os.environ.get('SLUGRUNNER_IMAGE_NAME', 'quay.io/deisci/slugrunner:canary') # noqa IMAGE_PULL_POLICY = os.environ.get('IMAGE_PULL_POLICY', "IfNotPresent") # noqa diff --git a/rootfs/scheduler/resources/ingress.py b/rootfs/scheduler/resources/ingress.py new file mode 100644 index 000000000..cf02fb772 --- /dev/null +++ b/rootfs/scheduler/resources/ingress.py @@ -0,0 +1,66 @@ +from scheduler.exceptions import KubeHTTPException +from scheduler.resources import Resource + + +class Ingress(Resource): + short_name = 'ingress' + + def get(self, name=None, **kwargs): + """ + Fetch a single Ingress or a list of Ingresses + """ + if name is not None: + url = "/apis/extensions/v1beta1/namespaces/%s/ingresses/%s" % (name, name) + message = 'get Ingress ' + name + else: + url = "/apis/extensions/v1beta1/namespaces/%s/ingresses" % name + message = 'get Ingresses' + + response = self.http_get(url, params=self.query_params(**kwargs)) + if self.unhealthy(response.status_code): + raise KubeHTTPException(response, message) + + return response + + def create(self, ingress, namespace, hostname): + url = "/apis/extensions/v1beta1/namespaces/%s/ingresses" % namespace + + if hostname == "": + raise KubeHTTPException("empty hostname value") + + data = { + "kind": "Ingress", + "apiVersion": "extensions/v1beta1", + "metadata": { + "name": ingress + }, + "spec": { + "rules": [ + {"host": ingress + "." + hostname, + "http": { + "paths": [ + {"path": "/", + "backend": { + "serviceName": ingress, + "servicePort": 80 + }} + ] + } + } + ] + } + } + response = self.http_post(url, json=data) + + if not response.status_code == 201: + raise KubeHTTPException(response, "create Ingress {}".format(namespace)) + + return response + + def delete(self, namespace, ingress): + url = self.api("/namespaces/{}/ingresses/{}", namespace, ingress) + response = self.http_delete(url) + if self.unhealthy(response.status_code): + raise KubeHTTPException(response, 'delete Ingress "{}"', namespace) + + return response