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