From bda65a4e4068b8176b822868afae75b07c3b3c88 Mon Sep 17 00:00:00 2001 From: Lukas Krejci Date: Wed, 16 Sep 2020 15:21:57 +0200 Subject: [PATCH] Manage the Traefik gateway for implementing single host on OpenShift (#378) Co-authored-by: Michal Vala --- deploy/crds/org_v1_che_cr.yaml | 25 + deploy/crds/org_v1_che_crd.yaml | 44 +- .../che-operator.clusterserviceversion.yaml | 8 +- .../manifests/org_v1_che_crd.yaml | 44 +- .../che-operator.clusterserviceversion.yaml | 8 +- .../manifests/org_v1_che_crd.yaml | 44 +- deploy/operator-local.yaml | 4 + deploy/operator.yaml | 4 + pkg/apis/org/v1/che_types.go | 36 ++ pkg/apis/org/v1/zz_generated.deepcopy.go | 8 + pkg/apis/org/v1/zz_generated.openapi.go | 44 +- pkg/controller/che/che_controller.go | 155 +---- pkg/controller/che/che_controller_test.go | 18 +- pkg/controller/che/create.go | 81 --- pkg/deploy/che_configmap.go | 12 +- pkg/deploy/configmap.go | 2 +- pkg/deploy/defaults.go | 47 +- pkg/deploy/deployment.go | 4 +- pkg/deploy/devfile_registry.go | 105 +++- pkg/deploy/gateway.go | 582 ++++++++++++++++++ pkg/deploy/identity_provider.go | 278 +++++++++ pkg/deploy/ingress.go | 18 +- pkg/deploy/plugin_registry.go | 108 +++- pkg/deploy/route.go | 16 + pkg/deploy/service.go | 4 +- pkg/util/util.go | 37 +- 26 files changed, 1441 insertions(+), 295 deletions(-) create mode 100644 pkg/deploy/gateway.go create mode 100644 pkg/deploy/identity_provider.go diff --git a/deploy/crds/org_v1_che_cr.yaml b/deploy/crds/org_v1_che_cr.yaml index 17fbbbb6f..3f45bf7fc 100644 --- a/deploy/crds/org_v1_che_cr.yaml +++ b/deploy/crds/org_v1_che_cr.yaml @@ -56,6 +56,24 @@ spec: workspaceNamespaceDefault: '' # defines if user is able to specify namespace different from the default allowUserDefinedWorkspaceNamespaces: false + # Sets the server and workspaces exposure type. Possible values are "multi-host", "single-host", "default-host". + # Defaults to "multi-host" which creates a separate ingress (or route on OpenShift) for every required + # endpoint. + # "single-host" makes Che exposed on a single hostname with workspaces exposed on subpaths. Please read the docs + # to learn about the limitations of this approach. Also consult the `singleHostExposureType` property to further configure + # how the operator and Che server make that happen on Kubernetes. + # "default-host" exposes che server on the host of the cluster. Please read the docs to learn about + # the limitations of this approach. + serverExposureStrategy: '' + # The image used for the gateway in the single host mode. + # Omit it or leave it empty to use the defaut container image provided by the operator. + singleHostGatewayImage: '' + # The image used for the gateway sidecar that provides configuration to the gateway. + # Omit it or leave it empty to use the defaut container image provided by the operator. + singleHostGatewayConfigSidecarImage: '' + # The labels that need to be present (and are put) on the configmaps representing the gateway configuration. + singleHostGatewayConfigMapLabels: '' + database: # when set to true, the operator skips deploying Postgres, and passes connection details of existing DB to Che server # otherwise a Postgres deployment is created @@ -124,6 +142,13 @@ spec: securityContextFsGroup: '' # User the Che POD and Workspace pod containers should run as securityContextRunAsUser: '' + # When the serverExposureStrategy is set to "single-host", the way the server, registries and workspaces + # are exposed is further configured by this property. The possible values are "native" (which means + # that the server and workspaces are exposed using ingresses on K8s) or "gateway" where the server + # and workspaces are exposed using a custom gateway based on Traefik. All the endpoints whether backed by the ingress + # or gateway "route" always point to the subpaths on the same domain. + # Defaults to "native". + singleHostExposureType: '' metrics: # Enables '/metrics' endpoint of Che server. enable: true diff --git a/deploy/crds/org_v1_che_crd.yaml b/deploy/crds/org_v1_che_crd.yaml index 1bed05144..428af85e3 100644 --- a/deploy/crds/org_v1_che_crd.yaml +++ b/deploy/crds/org_v1_che_crd.yaml @@ -230,7 +230,10 @@ spec: description: Strategy for ingress creation. This can be `multi-host` (host is explicitly provided in ingress), `single-host` (host is provided, path-based rules) and `default-host.*`(no host is - provided, path-based rules). Defaults to `"multi-host` + provided, path-based rules). Defaults to `"multi-host` Deprecated + in favor of "serverExposureStrategy" in the "server" section, + which defines this regardless of the cluster type. If both are + defined, `serverExposureStrategy` takes precedence. type: string securityContextFsGroup: description: FSGroup the Che pod and Workspace pods containers should @@ -240,6 +243,16 @@ spec: description: ID of the user the Che pod and Workspace pods containers should run as. Default to `1724`. type: string + singleHostExposureType: + description: When the serverExposureStrategy is set to "single-host", + the way the server, registries and workspaces are exposed is further + configured by this property. The possible values are "native" + (which means that the server and workspaces are exposed using + ingresses on K8s) or "gateway" where the server and workspaces + are exposed using a custom gateway based on Traefik. All the endpoints + whether backed by the ingress or gateway "route" always point + to the subpaths on the same domain. Defaults to "native". + type: string tlsSecretName: description: Name of a secret that will be used to setup ingress TLS termination if TLS is enabled. See also the `tlsSupport` field. @@ -445,6 +458,19 @@ spec: operator will automatically detect if router certificate is self-signed. If so it will be propagated to Che server and some other components. type: boolean + serverExposureStrategy: + description: Sets the server and workspaces exposure type. Possible + values are "multi-host", "single-host", "default-host". Defaults + to "multi-host" which creates a separate ingress (or route on + OpenShift) for every required endpoint. "single-host" makes Che + exposed on a single hostname with workspaces exposed on subpaths. + Please read the docs to learn about the limitations of this approach. + Also consult the `singleHostExposureType` property to further + configure how the operator and Che server make that happen on + Kubernetes. "default-host" exposes che server on the host of the + cluster. Please read the docs to learn about the limitations of + this approach. + type: string serverMemoryLimit: description: Overrides the memory limit used in the Che server deployment. Defaults to 1Gi. @@ -460,6 +486,22 @@ spec: signed with self-signed cert. So, Che server must be aware of its CA cert to be able to request it. This is disabled by default. type: string + singleHostGatewayConfigMapLabels: + additionalProperties: + type: string + description: The labels that need to be present (and are put) on + the configmaps representing the gateway configuration. + type: object + singleHostGatewayConfigSidecarImage: + description: The image used for the gateway sidecar that provides + configuration to the gateway. Omit it or leave it empty to use + the defaut container image provided by the operator. + type: string + singleHostGatewayImage: + description: The image used for the gateway in the single host mode. + Omit it or leave it empty to use the defaut container image provided + by the operator. + type: string tlsSupport: description: Deprecated. Instructs the operator to deploy Che in TLS mode. This is enabled by default. Disabling TLS may cause diff --git a/deploy/olm-catalog/eclipse-che-preview-kubernetes/manifests/che-operator.clusterserviceversion.yaml b/deploy/olm-catalog/eclipse-che-preview-kubernetes/manifests/che-operator.clusterserviceversion.yaml index 19da7ef6f..37bde15a1 100644 --- a/deploy/olm-catalog/eclipse-che-preview-kubernetes/manifests/che-operator.clusterserviceversion.yaml +++ b/deploy/olm-catalog/eclipse-che-preview-kubernetes/manifests/che-operator.clusterserviceversion.yaml @@ -13,7 +13,7 @@ metadata: operatorframework.io/suggested-namespace: eclipse-che repository: https://github.com/eclipse/che-operator support: Eclipse Foundation - name: eclipse-che-preview-kubernetes.v7.19.0-3.nightly + name: eclipse-che-preview-kubernetes.v7.19.0-4.nightly namespace: placeholder spec: apiservicedefinitions: {} @@ -226,6 +226,10 @@ spec: value: quay.io/eclipse/che-plugin-artifacts-broker:v3.4.0 - name: RELATED_IMAGE_che_server_secure_exposer_jwt_proxy_image value: quay.io/eclipse/che-jwtproxy:0.10.0 + - name: RELATED_IMAGE_single_host_gateway + value: docker.io/traefik:v2.2.8 + - name: RELATED_IMAGE_single_host_gateway_config_sidecar + value: quay.io/che-incubator/configbump:0.1.4 - name: CHE_FLAVOR value: che - name: CONSOLE_LINK_NAME @@ -353,4 +357,4 @@ spec: maturity: stable provider: name: Eclipse Foundation - version: 7.19.0-3.nightly + version: 7.19.0-4.nightly diff --git a/deploy/olm-catalog/eclipse-che-preview-kubernetes/manifests/org_v1_che_crd.yaml b/deploy/olm-catalog/eclipse-che-preview-kubernetes/manifests/org_v1_che_crd.yaml index 1bed05144..428af85e3 100644 --- a/deploy/olm-catalog/eclipse-che-preview-kubernetes/manifests/org_v1_che_crd.yaml +++ b/deploy/olm-catalog/eclipse-che-preview-kubernetes/manifests/org_v1_che_crd.yaml @@ -230,7 +230,10 @@ spec: description: Strategy for ingress creation. This can be `multi-host` (host is explicitly provided in ingress), `single-host` (host is provided, path-based rules) and `default-host.*`(no host is - provided, path-based rules). Defaults to `"multi-host` + provided, path-based rules). Defaults to `"multi-host` Deprecated + in favor of "serverExposureStrategy" in the "server" section, + which defines this regardless of the cluster type. If both are + defined, `serverExposureStrategy` takes precedence. type: string securityContextFsGroup: description: FSGroup the Che pod and Workspace pods containers should @@ -240,6 +243,16 @@ spec: description: ID of the user the Che pod and Workspace pods containers should run as. Default to `1724`. type: string + singleHostExposureType: + description: When the serverExposureStrategy is set to "single-host", + the way the server, registries and workspaces are exposed is further + configured by this property. The possible values are "native" + (which means that the server and workspaces are exposed using + ingresses on K8s) or "gateway" where the server and workspaces + are exposed using a custom gateway based on Traefik. All the endpoints + whether backed by the ingress or gateway "route" always point + to the subpaths on the same domain. Defaults to "native". + type: string tlsSecretName: description: Name of a secret that will be used to setup ingress TLS termination if TLS is enabled. See also the `tlsSupport` field. @@ -445,6 +458,19 @@ spec: operator will automatically detect if router certificate is self-signed. If so it will be propagated to Che server and some other components. type: boolean + serverExposureStrategy: + description: Sets the server and workspaces exposure type. Possible + values are "multi-host", "single-host", "default-host". Defaults + to "multi-host" which creates a separate ingress (or route on + OpenShift) for every required endpoint. "single-host" makes Che + exposed on a single hostname with workspaces exposed on subpaths. + Please read the docs to learn about the limitations of this approach. + Also consult the `singleHostExposureType` property to further + configure how the operator and Che server make that happen on + Kubernetes. "default-host" exposes che server on the host of the + cluster. Please read the docs to learn about the limitations of + this approach. + type: string serverMemoryLimit: description: Overrides the memory limit used in the Che server deployment. Defaults to 1Gi. @@ -460,6 +486,22 @@ spec: signed with self-signed cert. So, Che server must be aware of its CA cert to be able to request it. This is disabled by default. type: string + singleHostGatewayConfigMapLabels: + additionalProperties: + type: string + description: The labels that need to be present (and are put) on + the configmaps representing the gateway configuration. + type: object + singleHostGatewayConfigSidecarImage: + description: The image used for the gateway sidecar that provides + configuration to the gateway. Omit it or leave it empty to use + the defaut container image provided by the operator. + type: string + singleHostGatewayImage: + description: The image used for the gateway in the single host mode. + Omit it or leave it empty to use the defaut container image provided + by the operator. + type: string tlsSupport: description: Deprecated. Instructs the operator to deploy Che in TLS mode. This is enabled by default. Disabling TLS may cause diff --git a/deploy/olm-catalog/eclipse-che-preview-openshift/manifests/che-operator.clusterserviceversion.yaml b/deploy/olm-catalog/eclipse-che-preview-openshift/manifests/che-operator.clusterserviceversion.yaml index 9132c90bd..fe65b1146 100644 --- a/deploy/olm-catalog/eclipse-che-preview-openshift/manifests/che-operator.clusterserviceversion.yaml +++ b/deploy/olm-catalog/eclipse-che-preview-openshift/manifests/che-operator.clusterserviceversion.yaml @@ -13,7 +13,7 @@ metadata: operatorframework.io/suggested-namespace: eclipse-che repository: https://github.com/eclipse/che-operator support: Eclipse Foundation - name: eclipse-che-preview-openshift.v7.19.0-3.nightly + name: eclipse-che-preview-openshift.v7.19.0-4.nightly namespace: placeholder spec: apiservicedefinitions: {} @@ -266,6 +266,10 @@ spec: value: quay.io/eclipse/che-plugin-artifacts-broker:v3.4.0 - name: RELATED_IMAGE_che_server_secure_exposer_jwt_proxy_image value: quay.io/eclipse/che-jwtproxy:0.10.0 + - name: RELATED_IMAGE_single_host_gateway + value: docker.io/traefik:v2.2.8 + - name: RELATED_IMAGE_single_host_gateway_config_sidecar + value: quay.io/che-incubator/configbump:0.1.4 - name: CHE_FLAVOR value: che - name: CONSOLE_LINK_NAME @@ -400,4 +404,4 @@ spec: maturity: stable provider: name: Eclipse Foundation - version: 7.19.0-3.nightly + version: 7.19.0-4.nightly diff --git a/deploy/olm-catalog/eclipse-che-preview-openshift/manifests/org_v1_che_crd.yaml b/deploy/olm-catalog/eclipse-che-preview-openshift/manifests/org_v1_che_crd.yaml index 1bed05144..428af85e3 100644 --- a/deploy/olm-catalog/eclipse-che-preview-openshift/manifests/org_v1_che_crd.yaml +++ b/deploy/olm-catalog/eclipse-che-preview-openshift/manifests/org_v1_che_crd.yaml @@ -230,7 +230,10 @@ spec: description: Strategy for ingress creation. This can be `multi-host` (host is explicitly provided in ingress), `single-host` (host is provided, path-based rules) and `default-host.*`(no host is - provided, path-based rules). Defaults to `"multi-host` + provided, path-based rules). Defaults to `"multi-host` Deprecated + in favor of "serverExposureStrategy" in the "server" section, + which defines this regardless of the cluster type. If both are + defined, `serverExposureStrategy` takes precedence. type: string securityContextFsGroup: description: FSGroup the Che pod and Workspace pods containers should @@ -240,6 +243,16 @@ spec: description: ID of the user the Che pod and Workspace pods containers should run as. Default to `1724`. type: string + singleHostExposureType: + description: When the serverExposureStrategy is set to "single-host", + the way the server, registries and workspaces are exposed is further + configured by this property. The possible values are "native" + (which means that the server and workspaces are exposed using + ingresses on K8s) or "gateway" where the server and workspaces + are exposed using a custom gateway based on Traefik. All the endpoints + whether backed by the ingress or gateway "route" always point + to the subpaths on the same domain. Defaults to "native". + type: string tlsSecretName: description: Name of a secret that will be used to setup ingress TLS termination if TLS is enabled. See also the `tlsSupport` field. @@ -445,6 +458,19 @@ spec: operator will automatically detect if router certificate is self-signed. If so it will be propagated to Che server and some other components. type: boolean + serverExposureStrategy: + description: Sets the server and workspaces exposure type. Possible + values are "multi-host", "single-host", "default-host". Defaults + to "multi-host" which creates a separate ingress (or route on + OpenShift) for every required endpoint. "single-host" makes Che + exposed on a single hostname with workspaces exposed on subpaths. + Please read the docs to learn about the limitations of this approach. + Also consult the `singleHostExposureType` property to further + configure how the operator and Che server make that happen on + Kubernetes. "default-host" exposes che server on the host of the + cluster. Please read the docs to learn about the limitations of + this approach. + type: string serverMemoryLimit: description: Overrides the memory limit used in the Che server deployment. Defaults to 1Gi. @@ -460,6 +486,22 @@ spec: signed with self-signed cert. So, Che server must be aware of its CA cert to be able to request it. This is disabled by default. type: string + singleHostGatewayConfigMapLabels: + additionalProperties: + type: string + description: The labels that need to be present (and are put) on + the configmaps representing the gateway configuration. + type: object + singleHostGatewayConfigSidecarImage: + description: The image used for the gateway sidecar that provides + configuration to the gateway. Omit it or leave it empty to use + the defaut container image provided by the operator. + type: string + singleHostGatewayImage: + description: The image used for the gateway in the single host mode. + Omit it or leave it empty to use the defaut container image provided + by the operator. + type: string tlsSupport: description: Deprecated. Instructs the operator to deploy Che in TLS mode. This is enabled by default. Disabling TLS may cause diff --git a/deploy/operator-local.yaml b/deploy/operator-local.yaml index 90e2991be..3a1895369 100644 --- a/deploy/operator-local.yaml +++ b/deploy/operator-local.yaml @@ -65,6 +65,10 @@ spec: value: quay.io/eclipse/che-plugin-artifacts-broker:v3.4.0 - name: RELATED_IMAGE_che_server_secure_exposer_jwt_proxy_image value: quay.io/eclipse/che-jwtproxy:0.10.0 + - name: RELATED_IMAGE_single_host_gateway + value: docker.io/traefik:v2.2.8 + - name: RELATED_IMAGE_single_host_gateway_config_sidecar + value: quay.io/che-incubator/configbump:0.1.4 - name: CHE_FLAVOR value: che - name: CONSOLE_LINK_NAME diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 63fd71169..6a497e187 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -64,6 +64,10 @@ spec: value: quay.io/eclipse/che-plugin-artifacts-broker:v3.4.0 - name: RELATED_IMAGE_che_server_secure_exposer_jwt_proxy_image value: quay.io/eclipse/che-jwtproxy:0.10.0 + - name: RELATED_IMAGE_single_host_gateway + value: docker.io/traefik:v2.2.8 + - name: RELATED_IMAGE_single_host_gateway_config_sidecar + value: quay.io/che-incubator/configbump:0.1.4 - name: CHE_FLAVOR value: che - name: CONSOLE_LINK_NAME diff --git a/pkg/apis/org/v1/che_types.go b/pkg/apis/org/v1/che_types.go index b8e0cdb4d..b71f4d98d 100644 --- a/pkg/apis/org/v1/che_types.go +++ b/pkg/apis/org/v1/che_types.go @@ -21,6 +21,7 @@ package v1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" ) // +k8s:openapi-gen=true @@ -236,6 +237,31 @@ type CheClusterSpecServer struct { // Overrides the memory limit used in the Che server deployment. Defaults to 1Gi. // +optional ServerMemoryLimit string `json:"serverMemoryLimit,omitempty"` + + // Sets the server and workspaces exposure type. Possible values are "multi-host", "single-host", "default-host". + // Defaults to "multi-host" which creates a separate ingress (or route on OpenShift) for every required + // endpoint. + // "single-host" makes Che exposed on a single hostname with workspaces exposed on subpaths. Please read the docs + // to learn about the limitations of this approach. Also consult the `singleHostExposureType` property to further configure + // how the operator and Che server make that happen on Kubernetes. + // "default-host" exposes che server on the host of the cluster. Please read the docs to learn about + // the limitations of this approach. + // +optional + ServerExposureStrategy string `json:"serverExposureStrategy,omitempty"` + + // The image used for the gateway in the single host mode. + // Omit it or leave it empty to use the defaut container image provided by the operator. + // +optional + SingleHostGatewayImage string `json:"singleHostGatewayImage,omitempty"` + + // The image used for the gateway sidecar that provides configuration to the gateway. + // Omit it or leave it empty to use the defaut container image provided by the operator. + // +optional + SingleHostGatewayConfigSidecarImage string `json:"singleHostGatewayConfigSidecarImage,omitempty"` + + // The labels that need to be present (and are put) on the configmaps representing the gateway configuration. + // +optional + SingleHostGatewayConfigMapLabels labels.Set `json:"singleHostGatewayConfigMapLabels,omitempty"` } // +k8s:openapi-gen=true @@ -407,6 +433,8 @@ type CheClusterSpecK8SOnly struct { // Strategy for ingress creation. This can be `multi-host` (host is explicitly provided in ingress), // `single-host` (host is provided, path-based rules) and `default-host.*`(no host is provided, path-based rules). // Defaults to `"multi-host` + // Deprecated in favor of "serverExposureStrategy" in the "server" section, which defines this regardless of the cluster type. + // If both are defined, `serverExposureStrategy` takes precedence. // +optional IngressStrategy string `json:"ingressStrategy,omitempty"` // Ingress class that will define the which controler will manage ingresses. Defaults to `nginx`. @@ -423,6 +451,14 @@ type CheClusterSpecK8SOnly struct { // ID of the user the Che pod and Workspace pods containers should run as. Default to `1724`. // +optional SecurityContextRunAsUser string `json:"securityContextRunAsUser,omitempty"` + // When the serverExposureStrategy is set to "single-host", the way the server, registries and workspaces + // are exposed is further configured by this property. The possible values are "native" (which means + // that the server and workspaces are exposed using ingresses on K8s) or "gateway" where the server + // and workspaces are exposed using a custom gateway based on Traefik. All the endpoints whether backed by the ingress + // or gateway "route" always point to the subpaths on the same domain. + // Defaults to "native". + // +optional + SingleHostExposureType string `json:"singleHostExposureType,omitempty"` } type CheClusterSpecMetrics struct { diff --git a/pkg/apis/org/v1/zz_generated.deepcopy.go b/pkg/apis/org/v1/zz_generated.deepcopy.go index 07b36dd98..30dfaece1 100644 --- a/pkg/apis/org/v1/zz_generated.deepcopy.go +++ b/pkg/apis/org/v1/zz_generated.deepcopy.go @@ -5,6 +5,7 @@ package v1 import ( + labels "k8s.io/apimachinery/pkg/labels" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -165,6 +166,13 @@ func (in *CheClusterSpecServer) DeepCopyInto(out *CheClusterSpecServer) { (*out)[key] = val } } + if in.SingleHostGatewayConfigMapLabels != nil { + in, out := &in.SingleHostGatewayConfigMapLabels, &out.SingleHostGatewayConfigMapLabels + *out = make(labels.Set, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } diff --git a/pkg/apis/org/v1/zz_generated.openapi.go b/pkg/apis/org/v1/zz_generated.openapi.go index f46449961..4f75d8ea7 100644 --- a/pkg/apis/org/v1/zz_generated.openapi.go +++ b/pkg/apis/org/v1/zz_generated.openapi.go @@ -325,7 +325,7 @@ func schema_pkg_apis_org_v1_CheClusterSpecK8SOnly(ref common.ReferenceCallback) }, "ingressStrategy": { SchemaProps: spec.SchemaProps{ - Description: "Strategy for ingress creation. This can be `multi-host` (host is explicitly provided in ingress), `single-host` (host is provided, path-based rules) and `default-host.*`(no host is provided, path-based rules). Defaults to `\"multi-host`", + Description: "Strategy for ingress creation. This can be `multi-host` (host is explicitly provided in ingress), `single-host` (host is provided, path-based rules) and `default-host.*`(no host is provided, path-based rules). Defaults to `\"multi-host` Deprecated in favor of \"serverExposureStrategy\" in the \"server\" section, which defines this regardless of the cluster type. If both are defined, `serverExposureStrategy` takes precedence.", Type: []string{"string"}, Format: "", }, @@ -358,6 +358,13 @@ func schema_pkg_apis_org_v1_CheClusterSpecK8SOnly(ref common.ReferenceCallback) Format: "", }, }, + "singleHostExposureType": { + SchemaProps: spec.SchemaProps{ + Description: "When the serverExposureStrategy is set to \"single-host\", the way the server, registries and workspaces are exposed is further configured by this property. The possible values are \"native\" (which means that the server and workspaces are exposed using ingresses on K8s) or \"gateway\" where the server and workspaces are exposed using a custom gateway based on Traefik. All the endpoints whether backed by the ingress or gateway \"route\" always point to the subpaths on the same domain. Defaults to \"native\".", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, @@ -644,6 +651,41 @@ func schema_pkg_apis_org_v1_CheClusterSpecServer(ref common.ReferenceCallback) c Format: "", }, }, + "serverExposureStrategy": { + SchemaProps: spec.SchemaProps{ + Description: "Sets the server and workspaces exposure type. Possible values are \"multi-host\", \"single-host\", \"default-host\". Defaults to \"multi-host\" which creates a separate ingress (or route on OpenShift) for every required endpoint. \"single-host\" makes Che exposed on a single hostname with workspaces exposed on subpaths. Please read the docs to learn about the limitations of this approach. Also consult the `singleHostExposureType` property to further configure how the operator and Che server make that happen on Kubernetes. \"default-host\" exposes che server on the host of the cluster. Please read the docs to learn about the limitations of this approach.", + Type: []string{"string"}, + Format: "", + }, + }, + "singleHostGatewayImage": { + SchemaProps: spec.SchemaProps{ + Description: "The image used for the gateway in the single host mode. Omit it or leave it empty to use the defaut container image provided by the operator.", + Type: []string{"string"}, + Format: "", + }, + }, + "singleHostGatewayConfigSidecarImage": { + SchemaProps: spec.SchemaProps{ + Description: "The image used for the gateway sidecar that provides configuration to the gateway. Omit it or leave it empty to use the defaut container image provided by the operator.", + Type: []string{"string"}, + Format: "", + }, + }, + "singleHostGatewayConfigMapLabels": { + SchemaProps: spec.SchemaProps{ + Description: "The labels that need to be present (and are put) on the configmaps representing the gateway configuration.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, }, }, }, diff --git a/pkg/controller/che/che_controller.go b/pkg/controller/che/che_controller.go index f4a22ab45..07c185a05 100644 --- a/pkg/controller/che/che_controller.go +++ b/pkg/controller/che/che_controller.go @@ -222,6 +222,7 @@ type ReconcileChe struct { } const ( + failedValidationReason = "InstallOrUpdateFailed" failedNoOpenshiftUserReason = "InstallOrUpdateFailed" warningNoIdentityProvidersMessage = "No Openshift identity providers. Openshift oAuth was disabled. How to add identity provider read in the Help Link:" warningNoRealUsersMessage = "No real users. Openshift oAuth was disabled. How to add new user read in the Help Link:" @@ -269,6 +270,9 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e // Che cannot be deployed with current configuration. // Print error message in logs and wait until the configuration is changed. logrus.Error(err) + if err := r.SetStatusDetails(instance, request, failedValidationReason, err.Error(), ""); err != nil { + return reconcile.Result{}, err + } return reconcile.Result{}, nil } @@ -694,8 +698,6 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e } } - ingressStrategy := util.GetValue(instance.Spec.K8s.IngressStrategy, deploy.DefaultIngressStrategy) - ingressDomain := instance.Spec.K8s.IngressDomain tlsSupport := instance.Spec.Server.TlsSupport protocol := "http" if tlsSupport { @@ -706,7 +708,7 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e serviceStatus := deploy.SyncCheServiceToCluster(deployContext) if !tests { if !serviceStatus.Continue { - logrus.Infof("Waiting on service '%s' to be ready", deploy.CheServiceHame) + logrus.Infof("Waiting on service '%s' to be ready", deploy.CheServiceName) if serviceStatus.Err != nil { logrus.Error(serviceStatus.Err) } @@ -715,9 +717,10 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e } } + exposedServiceName := getServerExposingServiceName(instance) cheHost := "" if !isOpenShift { - ingress, err := deploy.SyncIngressToCluster(deployContext, cheFlavor, instance.Spec.Server.CheHost, deploy.CheServiceHame, 8080) + ingress, err := deploy.SyncIngressToCluster(deployContext, cheFlavor, instance.Spec.Server.CheHost, exposedServiceName, 8080) if !tests { if ingress == nil { logrus.Infof("Waiting on ingress '%s' to be ready", cheFlavor) @@ -736,7 +739,7 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e customHost = "" } - route, err := deploy.SyncRouteToCluster(deployContext, cheFlavor, customHost, deploy.CheServiceHame, 8080) + route, err := deploy.SyncRouteToCluster(deployContext, cheFlavor, customHost, exposedServiceName, 8080) if !tests { if route == nil { logrus.Infof("Waiting on route '%s' to be ready", cheFlavor) @@ -761,128 +764,17 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e } // create and provision Keycloak related objects - ExternalKeycloak := instance.Spec.Auth.ExternalIdentityProvider - - if !ExternalKeycloak { - if cheMultiUser == "false" { - if util.K8sclient.IsDeploymentExists("keycloak", instance.Namespace) { - util.K8sclient.DeleteDeployment("keycloak", instance.Namespace) - } - } else { - keycloakLabels := deploy.GetLabels(instance, "keycloak") - - serviceStatus := deploy.SyncServiceToCluster(deployContext, "keycloak", []string{"http"}, []int32{8080}, keycloakLabels) - if !tests { - if !serviceStatus.Continue { - logrus.Info("Waiting on service 'keycloak' to be ready") - if serviceStatus.Err != nil { - logrus.Error(serviceStatus.Err) - } - - return reconcile.Result{Requeue: serviceStatus.Requeue}, serviceStatus.Err - } - } - - // create Keycloak ingresses when on k8s - if !isOpenShift { - ingress, err := deploy.SyncIngressToCluster(deployContext, "keycloak", "", "keycloak", 8080) - if !tests { - if ingress == nil { - logrus.Info("Waiting on ingress 'keycloak' to be ready") - if err != nil { - logrus.Error(err) - } - - return reconcile.Result{RequeueAfter: time.Second * 1}, err - } - } - - keycloakURL := protocol + "://" + ingressDomain - if ingressStrategy == "multi-host" { - keycloakURL = protocol + "://keycloak-" + instance.Namespace + "." + ingressDomain - } - if instance.Spec.Auth.IdentityProviderURL != keycloakURL { - instance.Spec.Auth.IdentityProviderURL = keycloakURL - if err := r.UpdateCheCRSpec(instance, "Keycloak URL", keycloakURL); err != nil { - instance, _ = r.GetCR(request) - return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err - } - } - } else { - // create Keycloak route - route, err := deploy.SyncRouteToCluster(deployContext, "keycloak", "", "keycloak", 8080) - if !tests { - if route == nil { - logrus.Info("Waiting on route 'keycloak' to be ready") - if err != nil { - logrus.Error(err) - } - - return reconcile.Result{RequeueAfter: time.Second * 1}, err - } - - keycloakURL := protocol + "://" + route.Spec.Host - if instance.Spec.Auth.IdentityProviderURL != keycloakURL { - instance.Spec.Auth.IdentityProviderURL = keycloakURL - if err := r.UpdateCheCRSpec(instance, "Keycloak URL", keycloakURL); err != nil { - instance, _ = r.GetCR(request) - return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err - } - instance.Status.KeycloakURL = keycloakURL - if err := r.UpdateCheCRStatus(instance, "status: Keycloak URL", keycloakURL); err != nil { - instance, _ = r.GetCR(request) - return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err - } - } - } - } - - deploymentStatus := deploy.SyncKeycloakDeploymentToCluster(deployContext) - if !tests { - if !deploymentStatus.Continue { - logrus.Info("Waiting on deployment 'keycloak' to be ready") - if deploymentStatus.Err != nil { - logrus.Error(deploymentStatus.Err) - } - - return reconcile.Result{Requeue: deploymentStatus.Requeue}, deploymentStatus.Err - } - } - - if !tests { - if !instance.Status.KeycloakProvisoned { - if err := deploy.ProvisionKeycloakResources(deployContext); err != nil { - logrus.Error(err) - return reconcile.Result{RequeueAfter: time.Second}, err - } - - for { - instance.Status.KeycloakProvisoned = true - if err := r.UpdateCheCRStatus(instance, "status: provisioned with Keycloak", "true"); err != nil && - errors.IsConflict(err) { - instance, _ = r.GetCR(request) - continue - } - break - } - } - } - - if isOpenShift { - doInstallOpenShiftoAuthProvider := instance.Spec.Auth.OpenShiftoAuth - if doInstallOpenShiftoAuthProvider { - openShiftIdentityProviderStatus := instance.Status.OpenShiftoAuthProvisioned - if !openShiftIdentityProviderStatus { - if err := r.CreateIdentityProviderItems(instance, request, cheFlavor, deploy.KeycloakDeploymentName, isOpenShift4); err != nil { - return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, err - } - } - } + provisioned, err := deploy.SyncIdentityProviderToCluster(deployContext, cheHost, protocol, cheFlavor) + if !tests { + if !provisioned { + if err != nil { + logrus.Errorf("Error provisioning the identity provider to cluster: %v", err) } + return reconcile.Result{RequeueAfter: time.Second * 1}, err } } - provisioned, err := deploy.SyncDevfileRegistryToCluster(deployContext) + provisioned, err = deploy.SyncDevfileRegistryToCluster(deployContext, cheHost) if !tests { if !provisioned { if err != nil { @@ -892,7 +784,7 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e } } - provisioned, err = deploy.SyncPluginRegistryToCluster(deployContext) + provisioned, err = deploy.SyncPluginRegistryToCluster(deployContext, cheHost) if !tests { if !provisioned { if err != nil { @@ -931,6 +823,12 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e cmResourceVersion = cheConfigMap.ResourceVersion } + err = deploy.SyncGatewayToCluster(deployContext) + if err != nil { + logrus.Errorf("Failed to create the Server Gateway: %s", err) + return reconcile.Result{}, err + } + // Create a new che deployment deploymentStatus := deploy.SyncCheDeploymentToCluster(deployContext, cmResourceVersion) if !tests { @@ -1093,7 +991,7 @@ func EvaluateCheServerVersion(cr *orgv1.CheCluster) string { func getDefaultCheHost(deployContext *deploy.DeployContext) (string, error) { routeName := deploy.DefaultCheFlavor(deployContext.CheCluster) - route, err := deploy.SyncRouteToCluster(deployContext, routeName, "", deploy.CheServiceHame, 8080) + route, err := deploy.SyncRouteToCluster(deployContext, routeName, "", getServerExposingServiceName(deployContext.CheCluster), 8080) if route == nil { logrus.Infof("Waiting on route '%s' to be ready", routeName) if err != nil { @@ -1103,3 +1001,10 @@ func getDefaultCheHost(deployContext *deploy.DeployContext) (string, error) { } return route.Spec.Host, nil } + +func getServerExposingServiceName(cr *orgv1.CheCluster) string { + if cr.Spec.Server.ServerExposureStrategy == "single-host" && deploy.GetSingleHostExposureType(cr) == "gateway" { + return deploy.GatewayServiceName + } + return deploy.CheServiceName +} diff --git a/pkg/controller/che/che_controller_test.go b/pkg/controller/che/che_controller_test.go index 1f62fbab3..36915c8f2 100644 --- a/pkg/controller/che/che_controller_test.go +++ b/pkg/controller/che/che_controller_test.go @@ -232,8 +232,22 @@ func TestCheController(t *testing.T) { t.Errorf("ConfigMap wasn't updated properly. Expecting '%s', got: '%s'", expectedIdentityProviderName, cm.Data["CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER"]) } - err = r.client.Get(context.TODO(), types.NamespacedName{Name: cheCR.Name, Namespace: cheCR.Namespace}, cheCR) - err = r.CreateIdentityProviderItems(cheCR, req, "che", "keycloak", false) + clusterAPI := deploy.ClusterAPI{ + Client: r.client, + Scheme: r.scheme, + } + + deployContext := &deploy.DeployContext{ + CheCluster: cheCR, + ClusterAPI: clusterAPI, + } + + if err = r.client.Get(context.TODO(), types.NamespacedName{Name: cheCR.Name, Namespace: cheCR.Namespace}, cheCR); err != nil { + t.Errorf("Failed to get the Che custom resource %s: %s", cheCR.Name, err) + } + if err = deploy.CreateIdentityProviderItems(deployContext, "che"); err != nil { + t.Errorf("Failed to create the items for the identity provider: %s", err) + } oAuthClientName := cheCR.Spec.Auth.OAuthClientName oauthSecret := cheCR.Spec.Auth.OAuthSecret if err = r.client.Get(context.TODO(), types.NamespacedName{Name: oAuthClientName, Namespace: ""}, oAuthClient); err != nil { diff --git a/pkg/controller/che/create.go b/pkg/controller/che/create.go index efc1ce0fa..61cd36157 100644 --- a/pkg/controller/che/create.go +++ b/pkg/controller/che/create.go @@ -12,92 +12,11 @@ package che import ( - "context" - "strings" - - orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" "github.com/eclipse/che-operator/pkg/deploy" "github.com/eclipse/che-operator/pkg/util" - oauth "github.com/openshift/api/oauth/v1" - "github.com/sirupsen/logrus" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -func (r *ReconcileChe) CreateNewOauthClient(instance *orgv1.CheCluster, oAuthClient *oauth.OAuthClient) error { - oAuthClientFound := &oauth.OAuthClient{} - err := r.client.Get(context.TODO(), types.NamespacedName{Name: oAuthClient.Name, Namespace: oAuthClient.Namespace}, oAuthClientFound) - if err != nil && errors.IsNotFound(err) { - logrus.Infof("Creating a new object: %s, name: %s", oAuthClient.Kind, oAuthClient.Name) - err = r.client.Create(context.TODO(), oAuthClient) - if err != nil { - logrus.Errorf("Failed to create %s %s: %s", oAuthClient.Kind, oAuthClient.Name, err) - return err - } - return nil - } else if err != nil { - logrus.Errorf("An error occurred: %s", err) - - return err - } - return nil -} - -func (r *ReconcileChe) CreateIdentityProviderItems(instance *orgv1.CheCluster, request reconcile.Request, cheFlavor string, keycloakDeploymentName string, isOpenShift4 bool) (err error) { - tests := r.tests - oAuthClientName := instance.Spec.Auth.OAuthClientName - if len(oAuthClientName) < 1 { - oAuthClientName = instance.Name + "-openshift-identity-provider-" + strings.ToLower(util.GeneratePasswd(6)) - instance.Spec.Auth.OAuthClientName = oAuthClientName - if err := r.UpdateCheCRSpec(instance, "oAuthClient name", oAuthClientName); err != nil { - return err - } - } - oauthSecret := instance.Spec.Auth.OAuthSecret - if len(oauthSecret) < 1 { - oauthSecret = util.GeneratePasswd(12) - instance.Spec.Auth.OAuthSecret = oauthSecret - if err := r.UpdateCheCRSpec(instance, "oAuthC secret name", oauthSecret); err != nil { - return err - } - } - - keycloakURL := instance.Spec.Auth.IdentityProviderURL - keycloakRealm := util.GetValue(instance.Spec.Auth.IdentityProviderRealm, cheFlavor) - oAuthClient := deploy.NewOAuthClient(oAuthClientName, oauthSecret, keycloakURL, keycloakRealm, isOpenShift4) - if err := r.CreateNewOauthClient(instance, oAuthClient); err != nil { - return err - } - - if !tests { - openShiftIdentityProviderCommand, err := deploy.GetOpenShiftIdentityProviderProvisionCommand(instance, oAuthClientName, oauthSecret, isOpenShift4) - if err != nil { - logrus.Errorf("Failed to build identity provider provisioning command") - return err - } - podToExec, err := util.K8sclient.GetDeploymentPod(keycloakDeploymentName, instance.Namespace) - if err != nil { - logrus.Errorf("Failed to retrieve pod name. Further exec will fail") - return err - } - _, err = util.K8sclient.ExecIntoPod(podToExec, openShiftIdentityProviderCommand, "create OpenShift identity provider", instance.Namespace) - if err == nil { - for { - instance.Status.OpenShiftoAuthProvisioned = true - if err := r.UpdateCheCRStatus(instance, "status: provisioned with OpenShift identity provider", "true"); err != nil && - errors.IsConflict(err) { - instance, _ = r.GetCR(request) - continue - } - break - } - } - return err - } - return nil -} - func (r *ReconcileChe) GenerateAndSaveFields(deployContext *deploy.DeployContext, request reconcile.Request) (err error) { cheFlavor := deploy.DefaultCheFlavor(deployContext.CheCluster) if len(deployContext.CheCluster.Spec.Server.CheFlavor) < 1 { diff --git a/pkg/deploy/che_configmap.go b/pkg/deploy/che_configmap.go index 5f956a029..72c14cc4a 100644 --- a/pkg/deploy/che_configmap.go +++ b/pkg/deploy/che_configmap.go @@ -20,6 +20,7 @@ import ( "github.com/eclipse/che-operator/pkg/util" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" ) const ( @@ -74,6 +75,9 @@ type CheConfigMap struct { CheServerSecureExposerJwtProxyImage string `json:"CHE_SERVER_SECURE__EXPOSER_JWTPROXY_IMAGE,omitempty"` CheJGroupsKubernetesLabels string `json:"KUBERNETES_LABELS,omitempty"` CheTrustedCABundlesConfigMap string `json:"CHE_TRUSTED__CA__BUNDLES__CONFIGMAP,omitempty"` + ServerStrategy string `json:"CHE_INFRA_KUBERNETES_SERVER__STRATEGY"` + WorkspaceExposure string `json:"CHE_INFRA_KUBERNETES_SINGLEHOST_WORKSPACE_EXPOSURE"` + SingleHostGatewayConfigMapLabels string `json:"CHE_INFRA_KUBERNETES_SINGLEHOST_GATEWAY_CONFIGMAP__LABELS"` } func SyncCheConfigMapToCluster(deployContext *DeployContext) (*corev1.ConfigMap, error) { @@ -163,7 +167,7 @@ func GetCheConfigMapData(deployContext *DeployContext) (cheEnv map[string]string chePostgresDb := util.GetValue(deployContext.CheCluster.Spec.Database.ChePostgresDb, DefaultChePostgresDb) keycloakRealm := util.GetValue(deployContext.CheCluster.Spec.Auth.IdentityProviderRealm, cheFlavor) keycloakClientId := util.GetValue(deployContext.CheCluster.Spec.Auth.IdentityProviderClientId, cheFlavor+"-public") - ingressStrategy := util.GetValue(deployContext.CheCluster.Spec.K8s.IngressStrategy, DefaultIngressStrategy) + ingressStrategy := util.GetServerExposureStrategy(deployContext.CheCluster, DefaultServerExposureStrategy) ingressClass := util.GetValue(deployContext.CheCluster.Spec.K8s.IngressClass, DefaultIngressClass) devfileRegistryUrl := deployContext.CheCluster.Status.DevfileRegistryURL pluginRegistryUrl := deployContext.CheCluster.Status.PluginRegistryURL @@ -172,6 +176,8 @@ func GetCheConfigMapData(deployContext *DeployContext) (cheEnv map[string]string cheMetrics := strconv.FormatBool(deployContext.CheCluster.Spec.Metrics.Enable) cheLabels := util.MapToKeyValuePairs(GetLabels(deployContext.CheCluster, DefaultCheFlavor(deployContext.CheCluster))) cheMultiUser := GetCheMultiUser(deployContext.CheCluster) + workspaceExposure := GetSingleHostExposureType(deployContext.CheCluster) + singleHostGatewayConfigMapLabels := labels.FormatLabels(util.GetMapValue(deployContext.CheCluster.Spec.Server.SingleHostGatewayConfigMapLabels, DefaultSingleHostGatewayConfigMapLabels)) data := &CheConfigMap{ CheMultiUser: cheMultiUser, @@ -209,6 +215,9 @@ func GetCheConfigMapData(deployContext *DeployContext) (cheEnv map[string]string CheJGroupsKubernetesLabels: cheLabels, CheMetricsEnabled: cheMetrics, CheTrustedCABundlesConfigMap: deployContext.CheCluster.Spec.Server.ServerTrustStoreConfigMapName, + ServerStrategy: ingressStrategy, + WorkspaceExposure: workspaceExposure, + SingleHostGatewayConfigMapLabels: singleHostGatewayConfigMapLabels, } if cheMultiUser == "true" { @@ -235,7 +244,6 @@ func GetCheConfigMapData(deployContext *DeployContext) (cheEnv map[string]string "CHE_INFRA_KUBERNETES_POD_SECURITY__CONTEXT_FS__GROUP": securityContextFsGroup, "CHE_INFRA_KUBERNETES_POD_SECURITY__CONTEXT_RUN__AS__USER": securityContextRunAsUser, "CHE_INFRA_KUBERNETES_INGRESS_DOMAIN": ingressDomain, - "CHE_INFRA_KUBERNETES_SERVER__STRATEGY": ingressStrategy, "CHE_INFRA_KUBERNETES_TLS__SECRET": tlsSecretName, "CHE_INFRA_KUBERNETES_INGRESS_ANNOTATIONS__JSON": "{\"kubernetes.io/ingress.class\": " + ingressClass + ", \"nginx.ingress.kubernetes.io/rewrite-target\": \"/$1\",\"nginx.ingress.kubernetes.io/ssl-redirect\": " + tls + ",\"nginx.ingress.kubernetes.io/proxy-connect-timeout\": \"3600\",\"nginx.ingress.kubernetes.io/proxy-read-timeout\": \"3600\"}", "CHE_INFRA_KUBERNETES_INGRESS_PATH__TRANSFORM": "%s(.*)", diff --git a/pkg/deploy/configmap.go b/pkg/deploy/configmap.go index b2324de2c..9e28cb570 100644 --- a/pkg/deploy/configmap.go +++ b/pkg/deploy/configmap.go @@ -40,7 +40,7 @@ func SyncConfigMapToCluster(deployContext *DeployContext, specConfigMap *corev1. diff := cmp.Diff(clusterConfigMap.Data, specConfigMap.Data) if len(diff) > 0 { - logrus.Infof("Updating existed object: %s, name: %s", specConfigMap.Kind, specConfigMap.Name) + logrus.Infof("Updating existing object: %s, name: %s", specConfigMap.Kind, specConfigMap.Name) fmt.Printf("Difference:\n%s", diff) clusterConfigMap.Data = specConfigMap.Data err := deployContext.ClusterAPI.Client.Update(context.TODO(), clusterConfigMap) diff --git a/pkg/deploy/defaults.go b/pkg/deploy/defaults.go index 7e390beb7..fd58f7cee 100644 --- a/pkg/deploy/defaults.go +++ b/pkg/deploy/defaults.go @@ -27,18 +27,24 @@ import ( ) var ( - defaultCheServerImage string - defaultCheVersion string - defaultPluginRegistryImage string - defaultDevfileRegistryImage string - defaultCheTLSSecretsCreationJobImage string - defaultPvcJobsImage string - defaultPostgresImage string - defaultKeycloakImage string + defaultCheServerImage string + defaultCheVersion string + defaultPluginRegistryImage string + defaultDevfileRegistryImage string + defaultCheTLSSecretsCreationJobImage string + defaultPvcJobsImage string + defaultPostgresImage string + defaultKeycloakImage string + defaultSingleHostGatewayImage string + defaultSingleHostGatewayConfigSidecarImage string defaultCheWorkspacePluginBrokerMetadataImage string defaultCheWorkspacePluginBrokerArtifactsImage string defaultCheServerSecureExposerJwtProxyImage string + DefaultSingleHostGatewayConfigMapLabels = map[string]string{ + "app": "che", + "component": "che-gateway-config", + } ) const ( @@ -48,7 +54,6 @@ const ( DefaultChePostgresDb = "dbche" DefaultPvcStrategy = "common" DefaultPvcClaimSize = "1Gi" - DefaultIngressStrategy = "multi-host" DefaultIngressClass = "nginx" DefaultPluginRegistryMemoryLimit = "256Mi" @@ -76,6 +81,10 @@ const ( DefaultSecurityContextFsGroup = "1724" DefaultSecurityContextRunAsUser = "1724" + DefaultServerExposureStrategy = "multi-host" + DefaultKubernetesSingleHostExposureType = "native" + DefaultOpenShiftSingleHostExposureType = "gateway" + // This is only to correctly manage defaults during the transition // from Upstream 7.0.0 GA to the next version // That fixed bug https://github.com/eclipse/che/issues/13714 @@ -104,6 +113,8 @@ func InitDefaultsFromEnv() { defaultPvcJobsImage = getDefaultFromEnv(util.GetArchitectureDependentEnv("RELATED_IMAGE_pvc_jobs")) defaultPostgresImage = getDefaultFromEnv(util.GetArchitectureDependentEnv("RELATED_IMAGE_postgres")) defaultKeycloakImage = getDefaultFromEnv(util.GetArchitectureDependentEnv("RELATED_IMAGE_keycloak")) + defaultSingleHostGatewayImage = getDefaultFromEnv(util.GetArchitectureDependentEnv("RELATED_IMAGE_single_host_gateway")) + defaultSingleHostGatewayConfigSidecarImage = getDefaultFromEnv(util.GetArchitectureDependentEnv("RELATED_IMAGE_single_host_gateway_config_sidecar")) // CRW images for that are mentioned in the Che server che.properties // For CRW these should be synced by hand with images stored in RH registries @@ -128,6 +139,8 @@ func InitDefaultsFromFile(defaultsPath string) { defaultPvcJobsImage = util.GetDeploymentEnv(operatorDeployment, util.GetArchitectureDependentEnv("RELATED_IMAGE_pvc_jobs")) defaultPostgresImage = util.GetDeploymentEnv(operatorDeployment, util.GetArchitectureDependentEnv("RELATED_IMAGE_postgres")) defaultKeycloakImage = util.GetDeploymentEnv(operatorDeployment, util.GetArchitectureDependentEnv("RELATED_IMAGE_keycloak")) + defaultSingleHostGatewayImage = util.GetDeploymentEnv(operatorDeployment, util.GetArchitectureDependentEnv("RELATED_IMAGE_single_host_gateway")) + defaultSingleHostGatewayConfigSidecarImage = util.GetDeploymentEnv(operatorDeployment, util.GetArchitectureDependentEnv("RELATED_IMAGE_single_host_gateway_config_sidecar")) defaultCheWorkspacePluginBrokerMetadataImage = util.GetDeploymentEnv(operatorDeployment, util.GetArchitectureDependentEnv("RELATED_IMAGE_che_workspace_plugin_broker_metadata")) defaultCheWorkspacePluginBrokerArtifactsImage = util.GetDeploymentEnv(operatorDeployment, util.GetArchitectureDependentEnv("RELATED_IMAGE_che_workspace_plugin_broker_artifacts")) defaultCheServerSecureExposerJwtProxyImage = util.GetDeploymentEnv(operatorDeployment, util.GetArchitectureDependentEnv("RELATED_IMAGE_che_server_secure_exposer_jwt_proxy_image")) @@ -257,6 +270,14 @@ func DefaultCheServerSecureExposerJwtProxyImage(cr *orgv1.CheCluster) string { return patchDefaultImageName(cr, defaultCheServerSecureExposerJwtProxyImage) } +func DefaultSingleHostGatewayImage(cr *orgv1.CheCluster) string { + return patchDefaultImageName(cr, defaultSingleHostGatewayImage) +} + +func DefaultSingleHostGatewayConfigSidecarImage(cr *orgv1.CheCluster) string { + return patchDefaultImageName(cr, defaultSingleHostGatewayConfigSidecarImage) +} + func DefaultPullPolicyFromDockerImage(dockerImage string) string { tag := "latest" parts := strings.Split(dockerImage, ":") @@ -279,6 +300,14 @@ func GetCheMultiUser(cr *orgv1.CheCluster) string { return DefaultCheMultiUser } +func GetSingleHostExposureType(cr *orgv1.CheCluster) string { + if util.IsOpenShift { + return DefaultOpenShiftSingleHostExposureType + } + + return util.GetValue(cr.Spec.K8s.SingleHostExposureType, DefaultKubernetesSingleHostExposureType) +} + func patchDefaultImageName(cr *orgv1.CheCluster, imageName string) string { if !cr.IsAirGapMode() { return imageName diff --git a/pkg/deploy/deployment.go b/pkg/deploy/deployment.go index f987942ee..36d27b254 100644 --- a/pkg/deploy/deployment.go +++ b/pkg/deploy/deployment.go @@ -31,7 +31,7 @@ var deploymentDiffOpts = cmp.Options{ cmpopts.IgnoreFields(appsv1.DeploymentSpec{}, "Replicas", "RevisionHistoryLimit", "ProgressDeadlineSeconds"), cmpopts.IgnoreFields(appsv1.DeploymentStrategy{}, "RollingUpdate"), cmpopts.IgnoreFields(corev1.Container{}, "TerminationMessagePath", "TerminationMessagePolicy"), - cmpopts.IgnoreFields(corev1.PodSpec{}, "DNSPolicy", "SchedulerName", "SecurityContext"), + cmpopts.IgnoreFields(corev1.PodSpec{}, "DNSPolicy", "SchedulerName", "SecurityContext", "DeprecatedServiceAccount"), cmpopts.IgnoreFields(corev1.ConfigMapVolumeSource{}, "DefaultMode"), cmpopts.IgnoreFields(corev1.VolumeSource{}, "EmptyDir"), cmp.Comparer(func(x, y resource.Quantity) bool { @@ -72,7 +72,7 @@ func SyncDeploymentToCluster( if additionalDeploymentDiffOpts != nil { diff := cmp.Diff(clusterDeployment, specDeployment, additionalDeploymentDiffOpts) if len(diff) > 0 { - logrus.Infof("Updating existed object: %s, name: %s", specDeployment.Kind, specDeployment.Name) + logrus.Infof("Updating existing object: %s, name: %s", specDeployment.Kind, specDeployment.Name) fmt.Printf("Difference:\n%s", diff) clusterDeployment = additionalDeploymentMerge(specDeployment, clusterDeployment) err := deployContext.ClusterAPI.Client.Update(context.TODO(), clusterDeployment) diff --git a/pkg/deploy/devfile_registry.go b/pkg/deploy/devfile_registry.go index 615630c43..787a0a24e 100644 --- a/pkg/deploy/devfile_registry.go +++ b/pkg/deploy/devfile_registry.go @@ -27,57 +27,102 @@ type DevFileRegistryConfigMap struct { } const ( - DevfileRegistry = "devfile-registry" + DevfileRegistry = "devfile-registry" + devfileRegistryGatewayConfig = "che-gateway-route-devfile-registry" ) /** * Create devfile registry resources unless an external registry is used. */ -func SyncDevfileRegistryToCluster(deployContext *DeployContext) (bool, error) { +func SyncDevfileRegistryToCluster(deployContext *DeployContext, cheHost string) (bool, error) { devfileRegistryURL := deployContext.CheCluster.Spec.Server.DevfileRegistryUrl if !deployContext.CheCluster.Spec.Server.ExternalDevfileRegistry { - var host string + var endpoint string + var domain string + exposureStrategy := util.GetServerExposureStrategy(deployContext.CheCluster, DefaultServerExposureStrategy) + singleHostExposureType := GetSingleHostExposureType(deployContext.CheCluster) + useGateway := exposureStrategy == "single-host" && (util.IsOpenShift || singleHostExposureType == "gateway") + if exposureStrategy == "multi-host" { + // this won't get used on openshift, because there we're intentionally let Openshift decide on the domain name + domain = DevfileRegistry + "-" + deployContext.CheCluster.Namespace + "." + deployContext.CheCluster.Spec.K8s.IngressDomain + endpoint = domain + } else { + domain = cheHost + endpoint = domain + "/" + DevfileRegistry + } if !util.IsOpenShift { - ingress, err := SyncIngressToCluster(deployContext, DevfileRegistry, "", DevfileRegistry, 8080) - if !util.IsTestMode() { - if ingress == nil { - logrus.Infof("Waiting on ingress '%s' to be ready", DevfileRegistry) - if err != nil { - logrus.Error(err) - } - return false, err - } - } - ingressStrategy := util.GetValue(deployContext.CheCluster.Spec.K8s.IngressStrategy, DefaultIngressStrategy) - if ingressStrategy == "multi-host" { - host = DevfileRegistry + "-" + deployContext.CheCluster.Namespace + "." + deployContext.CheCluster.Spec.K8s.IngressDomain + if useGateway { + cfg := GetGatewayRouteConfig(deployContext, devfileRegistryGatewayConfig, "/"+DevfileRegistry, 10, "http://"+DevfileRegistry+":8080", true) + clusterCfg, err := SyncConfigMapToCluster(deployContext, &cfg) + if !util.IsTestMode() { + if clusterCfg == nil { + if err != nil { + logrus.Error(err) + } + return false, err + } + } + if err := DeleteIngressIfExists(DevfileRegistry, deployContext); !util.IsTestMode() && err != nil { + logrus.Error(err) + } } else { - host = deployContext.CheCluster.Spec.K8s.IngressDomain + "/" + DevfileRegistry + ingress, err := SyncIngressToCluster(deployContext, DevfileRegistry, domain, DevfileRegistry, 8080) + if !util.IsTestMode() { + if ingress == nil { + logrus.Infof("Waiting on ingress '%s' to be ready", DevfileRegistry) + if err != nil { + logrus.Error(err) + } + return false, err + } + } + if err := DeleteGatewayRouteConfig(devfileRegistryGatewayConfig, deployContext); !util.IsTestMode() && err != nil { + logrus.Error(err) + } } } else { - route, err := SyncRouteToCluster(deployContext, DevfileRegistry, "", DevfileRegistry, 8080) - if !util.IsTestMode() { - if route == nil { - logrus.Infof("Waiting on route '%s' to be ready", DevfileRegistry) - if err != nil { - logrus.Error(err) + if useGateway { + cfg := GetGatewayRouteConfig(deployContext, devfileRegistryGatewayConfig, "/"+DevfileRegistry, 10, "http://"+DevfileRegistry+":8080", true) + clusterCfg, err := SyncConfigMapToCluster(deployContext, &cfg) + if !util.IsTestMode() { + if clusterCfg == nil { + if err != nil { + logrus.Error(err) + } + return false, err } - - return false, err } - } + if err := DeleteRouteIfExists(DevfileRegistry, deployContext); !util.IsTestMode() && err != nil { + logrus.Error(err) + } + } else { + // the empty string for a host is intentional here - we let OpenShift decide on the hostname + route, err := SyncRouteToCluster(deployContext, DevfileRegistry, "", DevfileRegistry, 8080) + if !util.IsTestMode() { + if route == nil { + logrus.Infof("Waiting on route '%s' to be ready", DevfileRegistry) + if err != nil { + logrus.Error(err) + } - if !util.IsTestMode() { - host = route.Spec.Host + return false, err + } + } + if err := DeleteGatewayRouteConfig(devfileRegistryGatewayConfig, deployContext); !util.IsTestMode() && err != nil { + logrus.Error(err) + } + if !util.IsTestMode() { + endpoint = route.Spec.Host + } } } if devfileRegistryURL == "" { if deployContext.CheCluster.Spec.Server.TlsSupport { - devfileRegistryURL = "https://" + host + devfileRegistryURL = "https://" + endpoint } else { - devfileRegistryURL = "http://" + host + devfileRegistryURL = "http://" + endpoint } } diff --git a/pkg/deploy/gateway.go b/pkg/deploy/gateway.go new file mode 100644 index 000000000..60c1c2b33 --- /dev/null +++ b/pkg/deploy/gateway.go @@ -0,0 +1,582 @@ +package deploy + +import ( + "context" + "fmt" + "strconv" + + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + // GatewayServiceName is the name of the service which through which the gateway can be accessed + GatewayServiceName = "che-gateway" + + gatewayServerConfigName = "che-gateway-route-server" + gatewayConfigComponentName = "che-gateway-config" +) + +var ( + serviceAccountDiffOpts = cmpopts.IgnoreFields(corev1.ServiceAccount{}, "TypeMeta", "ObjectMeta", "Secrets", "ImagePullSecrets") + roleDiffOpts = cmpopts.IgnoreFields(rbac.Role{}, "TypeMeta", "ObjectMeta") + roleBindingDiffOpts = cmpopts.IgnoreFields(rbac.RoleBinding{}, "TypeMeta", "ObjectMeta") + serviceDiffOpts = cmp.Options{ + cmpopts.IgnoreFields(corev1.Service{}, "TypeMeta", "ObjectMeta", "Status"), + cmpopts.IgnoreFields(corev1.ServiceSpec{}, "ClusterIP"), + } + configMapDiffOpts = cmpopts.IgnoreFields(corev1.ConfigMap{}, "TypeMeta", "ObjectMeta") +) + +// SyncGatewayToCluster installs or deletes the gateway based on the custom resource configuration +func SyncGatewayToCluster(deployContext *DeployContext) error { + if deployContext.CheCluster.Spec.Server.ServerExposureStrategy == "single-host" && + (GetSingleHostExposureType(deployContext.CheCluster) == "gateway") { + return syncAll(deployContext) + } + + return deleteAll(deployContext) +} + +func syncAll(deployContext *DeployContext) error { + instance := deployContext.CheCluster + sa := getGatewayServiceAccountSpec(instance) + if err := sync(deployContext, &sa, serviceAccountDiffOpts); err != nil { + return err + } + + role := getGatewayRoleSpec(instance) + if err := sync(deployContext, &role, roleDiffOpts); err != nil { + return err + } + + roleBinding := getGatewayRoleBindingSpec(instance) + if err := sync(deployContext, &roleBinding, roleBindingDiffOpts); err != nil { + return err + } + + traefikConfig := getGatewayTraefikConfigSpec(instance) + if err := sync(deployContext, &traefikConfig, configMapDiffOpts); err != nil { + return err + } + + depl := getGatewayDeploymentSpec(instance) + if err := sync(deployContext, &depl, deploymentDiffOpts); err != nil { + return err + } + + service := getGatewayServiceSpec(instance) + if err := sync(deployContext, &service, serviceDiffOpts); err != nil { + return err + } + + serverConfig := getGatewayServerConfigSpec(deployContext) + if err := sync(deployContext, &serverConfig, configMapDiffOpts); err != nil { + return err + } + + return nil +} + +func deleteAll(deployContext *DeployContext) error { + instance := deployContext.CheCluster + clusterAPI := deployContext.ClusterAPI + + deployment := appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + }, + } + if err := delete(clusterAPI, &deployment); err != nil { + return err + } + + serverConfig := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayServerConfigName, + Namespace: instance.Namespace, + }, + } + if err := delete(clusterAPI, &serverConfig); err != nil { + return err + } + + traefikConfig := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che-gateway-config", + Namespace: instance.Namespace, + }, + } + if err := delete(clusterAPI, &traefikConfig); err == nil { + return err + } + + roleBinding := rbac.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + }, + } + if err := delete(clusterAPI, &roleBinding); err == nil { + return err + } + + role := rbac.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + }, + } + if err := delete(clusterAPI, &role); err == nil { + return err + } + + sa := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + }, + } + if err := delete(clusterAPI, &sa); err == nil { + return err + } + + return nil +} + +// sync syncs the blueprint to the cluster in a generic (as much as Go allows) manner. +func sync(deployContext *DeployContext, blueprint metav1.Object, diffOpts cmp.Option) error { + clusterAPI := deployContext.ClusterAPI + + blueprintObject, ok := blueprint.(runtime.Object) + if !ok { + return fmt.Errorf("object %T is not a runtime.Object. Cannot sync it", blueprint) + } + + key := client.ObjectKey{Name: blueprint.GetName(), Namespace: blueprint.GetNamespace()} + + actual := blueprintObject.DeepCopyObject() + + if getErr := deployContext.ClusterAPI.Client.Get(context.TODO(), key, actual); getErr != nil { + if statusErr, ok := getErr.(*errors.StatusError); !ok || statusErr.Status().Reason != metav1.StatusReasonNotFound { + return getErr + } + actual = nil + } + + kind := blueprintObject.GetObjectKind().GroupVersionKind().Kind + + if actual == nil { + logrus.Infof("Creating a new object: %s, name %s", kind, blueprint.GetName()) + obj, err := setOwnerReferenceAndConvertToRuntime(deployContext, blueprint) + if err != nil { + return err + } + + err = clusterAPI.Client.Create(context.TODO(), obj) + if err != nil { + if !errors.IsAlreadyExists(err) { + return err + } + + // ok, we got an already-exists error. So let's try to load the object into "actual". + // if we fail this retry for whatever reason, just give up rather than retrying this in a loop... + // the reconciliation loop will lead us here again in the next round. + if getErr := deployContext.ClusterAPI.Client.Get(context.TODO(), key, actual); getErr != nil { + return getErr + } + } + } + + if actual != nil { + actualMeta := actual.(metav1.Object) + + diff := cmp.Diff(actual, blueprint, diffOpts) + if len(diff) > 0 { + logrus.Infof("Updating existing object: %s, name: %s", kind, actualMeta.GetName()) + fmt.Printf("Difference:\n%s", diff) + + if isUpdateUsingDeleteCreate(actual.GetObjectKind().GroupVersionKind().Kind) { + err := clusterAPI.Client.Delete(context.TODO(), actual) + if err != nil { + return err + } + + obj, err := setOwnerReferenceAndConvertToRuntime(deployContext, blueprint) + if err != nil { + return err + } + + err = clusterAPI.Client.Create(context.TODO(), obj) + if err != nil { + return err + } + } else { + obj, err := setOwnerReferenceAndConvertToRuntime(deployContext, blueprint) + if err != nil { + return err + } + + err = clusterAPI.Client.Update(context.TODO(), obj) + if err != nil { + return err + } + } + } + } + + return nil +} + +func isUpdateUsingDeleteCreate(kind string) bool { + return "Service" == kind || "Ingress" == kind || "Route" == kind +} + +func setOwnerReferenceAndConvertToRuntime(deployContext *DeployContext, obj metav1.Object) (runtime.Object, error) { + err := controllerutil.SetControllerReference(deployContext.CheCluster, obj, deployContext.ClusterAPI.Scheme) + if err != nil { + return nil, err + } + + robj, ok := obj.(runtime.Object) + if !ok { + return nil, fmt.Errorf("object %T is not a runtime.Object. Cannot sync it", obj) + } + + return robj, nil +} + +func delete(clusterAPI ClusterAPI, obj metav1.Object) error { + key := client.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()} + ro := obj.(runtime.Object) + if getErr := clusterAPI.Client.Get(context.TODO(), key, ro); getErr == nil { + if err := clusterAPI.Client.Delete(context.TODO(), ro); err != nil { + if !errors.IsNotFound(err) { + return err + } + } + } + + return nil +} + +// GetGatewayRouteConfig creates a config map with traefik configuration for a single new route. +// `serviceName` is an arbitrary name identifying the configuration. This should be unique within operator. Che server only creates +// new configuration for workspaces, so the name should not resemble any of the names created by the Che server. +func GetGatewayRouteConfig(deployContext *DeployContext, serviceName string, pathPrefix string, priority int, internalUrl string, stripPrefix bool) corev1.ConfigMap { + pathRewrite := pathPrefix != "/" && stripPrefix + + data := `--- +http: + routers: + ` + serviceName + `: + rule: "PathPrefix(` + "`" + pathPrefix + "`" + `)" + service: ` + serviceName + ` + priority: ` + strconv.Itoa(priority) + + if pathRewrite { + data += ` + middlewares: + - "` + serviceName + `"` + } + + data += ` + services: + ` + serviceName + `: + loadBalancer: + servers: + - url: '` + internalUrl + `'` + + if pathRewrite { + data += ` + middlewares: + ` + serviceName + `: + stripPrefix: + prefixes: + - "` + pathPrefix + `"` + } + + ret := corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: deployContext.CheCluster.Namespace, + Labels: util.MergeMaps( + GetLabels(deployContext.CheCluster, gatewayConfigComponentName), + util.GetMapValue(deployContext.CheCluster.Spec.Server.SingleHostGatewayConfigMapLabels, DefaultSingleHostGatewayConfigMapLabels)), + }, + Data: map[string]string{ + serviceName + ".yml": data, + }, + } + + controllerutil.SetControllerReference(deployContext.CheCluster, &ret, deployContext.ClusterAPI.Scheme) + + return ret +} + +func DeleteGatewayRouteConfig(serviceName string, deployContext *DeployContext) error { + obj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: deployContext.CheCluster.Namespace, + }, + } + + return delete(deployContext.ClusterAPI, obj) +} + +// below functions declare the desired states of the various objects required for the gateway + +func getGatewayServerConfigSpec(deployContext *DeployContext) corev1.ConfigMap { + return GetGatewayRouteConfig(deployContext, gatewayServerConfigName, "/", 1, "http://"+CheServiceName+":8080", false) +} + +func getGatewayServiceAccountSpec(instance *orgv1.CheCluster) corev1.ServiceAccount { + return corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ServiceAccount", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + Labels: GetLabels(instance, GatewayServiceName), + }, + } +} + +func getGatewayRoleSpec(instance *orgv1.CheCluster) rbac.Role { + return rbac.Role{ + TypeMeta: metav1.TypeMeta{ + APIVersion: rbac.SchemeGroupVersion.String(), + Kind: "Role", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + Labels: GetLabels(instance, GatewayServiceName), + }, + Rules: []rbac.PolicyRule{ + { + Verbs: []string{"watch", "get", "list"}, + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + }, + }, + } +} + +func getGatewayRoleBindingSpec(instance *orgv1.CheCluster) rbac.RoleBinding { + return rbac.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + APIVersion: rbac.SchemeGroupVersion.String(), + Kind: "RoleBinding", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + Labels: GetLabels(instance, GatewayServiceName), + }, + RoleRef: rbac.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: GatewayServiceName, + }, + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + Name: GatewayServiceName, + }, + }, + } +} + +func getGatewayTraefikConfigSpec(instance *orgv1.CheCluster) corev1.ConfigMap { + return corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "che-gateway-config", + Namespace: instance.Namespace, + Labels: GetLabels(instance, GatewayServiceName), + }, + Data: map[string]string{ + "traefik.yml": ` +entrypoints: + http: + address: ":8080" + forwardedHeaders: + insecure: true + https: + address: ":8443" + forwardedHeaders: + insecure: true +global: + checkNewVersion: false + sendAnonymousUsage: false +providers: + file: + directory: "/dynamic-config" + watch: true +log: + level: "INFO"`, + }, + } +} + +func getGatewayDeploymentSpec(instance *orgv1.CheCluster) appsv1.Deployment { + gatewayImage := util.GetValue(instance.Spec.Server.SingleHostGatewayImage, DefaultSingleHostGatewayImage(instance)) + sidecarImage := util.GetValue(instance.Spec.Server.SingleHostGatewayConfigSidecarImage, DefaultSingleHostGatewayConfigSidecarImage(instance)) + configLabelsMap := util.GetMapValue(instance.Spec.Server.SingleHostGatewayConfigMapLabels, DefaultSingleHostGatewayConfigMapLabels) + terminationGracePeriodSeconds := int64(10) + + configLabels := labels.FormatLabels(configLabelsMap) + + return appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + Labels: GetLabels(instance, GatewayServiceName), + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: GetLabels(instance, GatewayServiceName), + }, + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: GetLabels(instance, GatewayServiceName), + }, + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, + ServiceAccountName: GatewayServiceName, + RestartPolicy: corev1.RestartPolicyAlways, + Containers: []corev1.Container{ + { + Name: "gateway", + Image: gatewayImage, + ImagePullPolicy: corev1.PullAlways, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "static-config", + MountPath: "/etc/traefik", + }, + { + Name: "dynamic-config", + MountPath: "/dynamic-config", + }, + }, + }, + { + Name: "configbump", + Image: sidecarImage, + ImagePullPolicy: corev1.PullAlways, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "dynamic-config", + MountPath: "/dynamic-config", + }, + }, + Env: []corev1.EnvVar{ + { + Name: "CONFIG_BUMP_DIR", + Value: "/dynamic-config", + }, + { + Name: "CONFIG_BUMP_LABELS", + Value: configLabels, + }, + { + Name: "CONFIG_BUMP_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.namespace", + }, + }, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "static-config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "che-gateway-config", + }, + }, + }, + }, + { + Name: "dynamic-config", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + }, + }, + } +} + +func getGatewayServiceSpec(instance *orgv1.CheCluster) corev1.Service { + return corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayServiceName, + Namespace: instance.Namespace, + Labels: GetLabels(instance, GatewayServiceName), + }, + Spec: corev1.ServiceSpec{ + Selector: GetLabels(instance, GatewayServiceName), + SessionAffinity: corev1.ServiceAffinityNone, + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Name: "gateway-http", + Port: 8080, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(8080), + }, + { + Name: "gateway-https", + Port: 8443, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(8443), + }, + }, + }, + } +} diff --git a/pkg/deploy/identity_provider.go b/pkg/deploy/identity_provider.go new file mode 100644 index 000000000..24cc11c0d --- /dev/null +++ b/pkg/deploy/identity_provider.go @@ -0,0 +1,278 @@ +package deploy + +import ( + "context" + "strings" + + "github.com/eclipse/che-operator/pkg/util" + oauth "github.com/openshift/api/oauth/v1" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" +) + +const ( + keycloakGatewayConfig = "che-gateway-route-keycloak" +) + +// SyncIdentityProviderToCluster instantiates the identity provider (Keycloak) in the cluster. Returns true if +// the provisioning is complete, false if requeue of the reconcile request is needed. +func SyncIdentityProviderToCluster(deployContext *DeployContext, cheHost string, protocol string, cheFlavor string) (bool, error) { + instance := deployContext.CheCluster + cheMultiUser := GetCheMultiUser(instance) + tests := util.IsTestMode() + isOpenShift := util.IsOpenShift + + if instance.Spec.Auth.ExternalIdentityProvider { + return true, nil + } + + if cheMultiUser == "false" { + if util.K8sclient.IsDeploymentExists("keycloak", instance.Namespace) { + util.K8sclient.DeleteDeployment("keycloak", instance.Namespace) + } + + return true, nil + } + + keycloakLabels := GetLabels(instance, "keycloak") + + serviceStatus := SyncServiceToCluster(deployContext, "keycloak", []string{"http"}, []int32{8080}, keycloakLabels) + if !tests { + if !serviceStatus.Continue { + logrus.Info("Waiting on service 'keycloak' to be ready") + if serviceStatus.Err != nil { + logrus.Error(serviceStatus.Err) + } + + return false, serviceStatus.Err + } + } + + exposureStrategy := util.GetServerExposureStrategy(instance, DefaultServerExposureStrategy) + singleHostExposureType := GetSingleHostExposureType(instance) + useGateway := exposureStrategy == "single-host" && (util.IsOpenShift || singleHostExposureType == "gateway") + + // create Keycloak ingresses when on k8s + var keycloakURL string + if !isOpenShift { + var host string + if exposureStrategy == "multi-host" { + host = "keycloak-" + deployContext.CheCluster.Namespace + "." + deployContext.CheCluster.Spec.K8s.IngressDomain + } else { + host = cheHost + } + if useGateway { + // try to guess where in the ingress-creating code the /auth endpoint is defined... + cfg := GetGatewayRouteConfig(deployContext, keycloakGatewayConfig, "/auth", 10, "http://keycloak:8080", false) + _, err := SyncConfigMapToCluster(deployContext, &cfg) + if !tests { + if err != nil { + logrus.Error(err) + } + } + + if err := DeleteIngressIfExists("keycloak", deployContext); !tests && err != nil { + logrus.Error(err) + } + + keycloakURL = protocol + "://" + cheHost + } else { + logrus.Infof("Deploying Keycloak on %s", host) + ingress, err := SyncIngressToCluster(deployContext, "keycloak", host, "keycloak", 8080) + if !tests { + if ingress == nil { + logrus.Info("Waiting on ingress 'keycloak' to be ready") + if err != nil { + logrus.Error(err) + } + + return false, err + } + } + + logrus.Infof("Deployed Keycloak on %s", ingress.Spec.Rules[0].Host) + + if err := DeleteGatewayRouteConfig(keycloakGatewayConfig, deployContext); !tests && err != nil { + logrus.Error(err) + } + + keycloakURL = protocol + "://" + host + } + } else { + if useGateway { + cfg := GetGatewayRouteConfig(deployContext, keycloakGatewayConfig, "/auth", 10, "http://keycloak:8080", false) + _, err := SyncConfigMapToCluster(deployContext, &cfg) + if !tests { + if err != nil { + logrus.Error(err) + } + } + keycloakURL = protocol + "://" + cheHost + + if err := DeleteRouteIfExists("keycloak", deployContext); !tests && err != nil { + logrus.Error(err) + } + } else { + // create Keycloak route + route, err := SyncRouteToCluster(deployContext, "keycloak", "", "keycloak", 8080) + if !tests { + if route == nil { + logrus.Info("Waiting on route 'keycloak' to be ready") + if err != nil { + logrus.Error(err) + } + + return false, err + } + + keycloakURL = protocol + "://" + route.Spec.Host + } + + if err := DeleteGatewayRouteConfig(keycloakGatewayConfig, deployContext); !tests && err != nil { + logrus.Error(err) + } + } + } + + if instance.Spec.Auth.IdentityProviderURL != keycloakURL { + instance.Spec.Auth.IdentityProviderURL = keycloakURL + if err := UpdateCheCRSpec(deployContext, "Keycloak URL", keycloakURL); err != nil { + return false, err + } + + instance.Status.KeycloakURL = keycloakURL + if err := UpdateCheCRStatus(deployContext, "Keycloak URL", keycloakURL); err != nil { + return false, err + } + } + + deploymentStatus := SyncKeycloakDeploymentToCluster(deployContext) + if !tests { + if !deploymentStatus.Continue { + logrus.Info("Waiting on deployment 'keycloak' to be ready") + if deploymentStatus.Err != nil { + logrus.Error(deploymentStatus.Err) + } + + return false, deploymentStatus.Err + } + } + + if !tests { + if !instance.Status.KeycloakProvisoned { + if err := ProvisionKeycloakResources(deployContext); err != nil { + logrus.Error(err) + return false, err + } + + for { + instance.Status.KeycloakProvisoned = true + if err := UpdateCheCRStatus(deployContext, "status: provisioned with Keycloak", "true"); err != nil && + errors.IsConflict(err) { + + reload(deployContext) + continue + } + break + } + } + } + + if isOpenShift { + doInstallOpenShiftoAuthProvider := instance.Spec.Auth.OpenShiftoAuth + if doInstallOpenShiftoAuthProvider { + openShiftIdentityProviderStatus := instance.Status.OpenShiftoAuthProvisioned + if !openShiftIdentityProviderStatus { + if err := CreateIdentityProviderItems(deployContext, cheFlavor); err != nil { + return false, err + } + } + } + } + + return true, nil +} + +func CreateIdentityProviderItems(deployContext *DeployContext, cheFlavor string) error { + instance := deployContext.CheCluster + tests := util.IsTestMode() + isOpenShift4 := util.IsOpenShift4 + keycloakDeploymentName := KeycloakDeploymentName + oAuthClientName := instance.Spec.Auth.OAuthClientName + if len(oAuthClientName) < 1 { + oAuthClientName = instance.Name + "-openshift-identity-provider-" + strings.ToLower(util.GeneratePasswd(6)) + instance.Spec.Auth.OAuthClientName = oAuthClientName + if err := UpdateCheCRSpec(deployContext, "oAuthClient name", oAuthClientName); err != nil { + return err + } + } + oauthSecret := instance.Spec.Auth.OAuthSecret + if len(oauthSecret) < 1 { + oauthSecret = util.GeneratePasswd(12) + instance.Spec.Auth.OAuthSecret = oauthSecret + if err := UpdateCheCRSpec(deployContext, "oAuthC secret name", oauthSecret); err != nil { + return err + } + } + + keycloakURL := instance.Spec.Auth.IdentityProviderURL + keycloakRealm := util.GetValue(instance.Spec.Auth.IdentityProviderRealm, cheFlavor) + oAuthClient := NewOAuthClient(oAuthClientName, oauthSecret, keycloakURL, keycloakRealm, isOpenShift4) + if err := createNewOauthClient(deployContext, oAuthClient); err != nil { + return err + } + + if !tests { + openShiftIdentityProviderCommand, err := GetOpenShiftIdentityProviderProvisionCommand(instance, oAuthClientName, oauthSecret, isOpenShift4) + if err != nil { + logrus.Errorf("Failed to build identity provider provisioning command") + return err + } + podToExec, err := util.K8sclient.GetDeploymentPod(keycloakDeploymentName, instance.Namespace) + if err != nil { + logrus.Errorf("Failed to retrieve pod name. Further exec will fail") + return err + } + _, err = util.K8sclient.ExecIntoPod(podToExec, openShiftIdentityProviderCommand, "create OpenShift identity provider", instance.Namespace) + if err == nil { + for { + instance.Status.OpenShiftoAuthProvisioned = true + if err := UpdateCheCRStatus(deployContext, "status: provisioned with OpenShift identity provider", "true"); err != nil && + errors.IsConflict(err) { + + reload(deployContext) + continue + } + break + } + } + } + return nil +} + +func createNewOauthClient(deployContext *DeployContext, oAuthClient *oauth.OAuthClient) error { + oAuthClientFound := &oauth.OAuthClient{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: oAuthClient.Name, Namespace: oAuthClient.Namespace}, oAuthClientFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object: %s, name: %s", oAuthClient.Kind, oAuthClient.Name) + err = deployContext.ClusterAPI.Client.Create(context.TODO(), oAuthClient) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", oAuthClient.Kind, oAuthClient.Name, err) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred: %s", err) + + return err + } + return nil +} + +func reload(deployContext *DeployContext) error { + return deployContext.ClusterAPI.Client.Get( + context.TODO(), + types.NamespacedName{Name: deployContext.CheCluster.Name, Namespace: deployContext.CheCluster.Namespace}, + deployContext.CheCluster) +} diff --git a/pkg/deploy/ingress.go b/pkg/deploy/ingress.go index 3df2f30a4..17d5e8632 100644 --- a/pkg/deploy/ingress.go +++ b/pkg/deploy/ingress.go @@ -72,6 +72,22 @@ func SyncIngressToCluster( return clusterIngress, nil } +func DeleteIngressIfExists(name string, deployContext *DeployContext) error { + ingress, err := getClusterIngress(name, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client) + if err != nil { + return err + } + + if ingress != nil { + err = deployContext.ClusterAPI.Client.Delete(context.TODO(), ingress) + if err != nil { + return err + } + } + + return nil +} + func getClusterIngress(name string, namespace string, client runtimeClient.Client) (*v1beta1.Ingress, error) { ingress := &v1beta1.Ingress{} namespacedName := types.NamespacedName{ @@ -96,7 +112,7 @@ func getSpecIngress( servicePort int) (*v1beta1.Ingress, error) { tlsSupport := deployContext.CheCluster.Spec.Server.TlsSupport - ingressStrategy := util.GetValue(deployContext.CheCluster.Spec.K8s.IngressStrategy, DefaultIngressStrategy) + ingressStrategy := util.GetServerExposureStrategy(deployContext.CheCluster, DefaultServerExposureStrategy) ingressDomain := deployContext.CheCluster.Spec.K8s.IngressDomain ingressClass := util.GetValue(deployContext.CheCluster.Spec.K8s.IngressClass, DefaultIngressClass) labels := GetLabels(deployContext.CheCluster, name) diff --git a/pkg/deploy/plugin_registry.go b/pkg/deploy/plugin_registry.go index 7895c9381..dd310eb83 100644 --- a/pkg/deploy/plugin_registry.go +++ b/pkg/deploy/plugin_registry.go @@ -27,57 +27,103 @@ type PluginRegistryConfigMap struct { } const ( - PluginRegistry = "plugin-registry" + PluginRegistry = "plugin-registry" + pluginRegistryGatewayConfig = "che-gateway-route-plugin-registry" ) /** * Create plugin registry resources unless an external registry is used. */ -func SyncPluginRegistryToCluster(deployContext *DeployContext) (bool, error) { +func SyncPluginRegistryToCluster(deployContext *DeployContext, cheHost string) (bool, error) { pluginRegistryURL := deployContext.CheCluster.Spec.Server.PluginRegistryUrl if !deployContext.CheCluster.Spec.Server.ExternalPluginRegistry { - var host string - if !util.IsOpenShift { - ingress, err := SyncIngressToCluster(deployContext, PluginRegistry, "", PluginRegistry, 8080) - if !util.IsTestMode() { - if ingress == nil { - logrus.Infof("Waiting on ingress '%s' to be ready", PluginRegistry) - if err != nil { - logrus.Error(err) - } - return false, err - } - } + var endpoint string + var domain string + exposureStrategy := util.GetServerExposureStrategy(deployContext.CheCluster, DefaultServerExposureStrategy) + singleHostExposureType := GetSingleHostExposureType(deployContext.CheCluster) + useGateway := exposureStrategy == "single-host" && (util.IsOpenShift || singleHostExposureType == "gateway") - ingressStrategy := util.GetValue(deployContext.CheCluster.Spec.K8s.IngressStrategy, DefaultIngressStrategy) - if ingressStrategy == "multi-host" { - host = PluginRegistry + "-" + deployContext.CheCluster.Namespace + "." + deployContext.CheCluster.Spec.K8s.IngressDomain + if exposureStrategy == "multi-host" { + // this won't get used on openshift, because there we're intentionally let Openshift decide on the domain name + domain = PluginRegistry + "-" + deployContext.CheCluster.Namespace + "." + deployContext.CheCluster.Spec.K8s.IngressDomain + endpoint = domain + } else { + domain = cheHost + endpoint = domain + "/" + PluginRegistry + } + if !util.IsOpenShift { + if useGateway { + cfg := GetGatewayRouteConfig(deployContext, pluginRegistryGatewayConfig, "/"+PluginRegistry, 10, "http://"+PluginRegistry+":8080", true) + clusterCfg, err := SyncConfigMapToCluster(deployContext, &cfg) + if !util.IsTestMode() { + if clusterCfg == nil { + if err != nil { + logrus.Error(err) + } + return false, err + } + } + if err := DeleteIngressIfExists(PluginRegistry, deployContext); !util.IsTestMode() && err != nil { + logrus.Error(err) + } } else { - host = deployContext.CheCluster.Spec.K8s.IngressDomain + "/" + PluginRegistry + ingress, err := SyncIngressToCluster(deployContext, PluginRegistry, domain, PluginRegistry, 8080) + if !util.IsTestMode() { + if ingress == nil { + logrus.Infof("Waiting on ingress '%s' to be ready", PluginRegistry) + if err != nil { + logrus.Error(err) + } + return false, err + } + } + if err := DeleteGatewayRouteConfig(pluginRegistryGatewayConfig, deployContext); !util.IsTestMode() && err != nil { + logrus.Error(err) + } } } else { - route, err := SyncRouteToCluster(deployContext, PluginRegistry, "", PluginRegistry, 8080) - if !util.IsTestMode() { - if route == nil { - logrus.Infof("Waiting on route '%s' to be ready", PluginRegistry) - if err != nil { - logrus.Error(err) + if useGateway { + cfg := GetGatewayRouteConfig(deployContext, pluginRegistryGatewayConfig, "/"+PluginRegistry, 10, "http://"+PluginRegistry+":8080", true) + clusterCfg, err := SyncConfigMapToCluster(deployContext, &cfg) + if !util.IsTestMode() { + if clusterCfg == nil { + if err != nil { + logrus.Error(err) + } + return false, err } - - return false, err } - } + if err := DeleteRouteIfExists(PluginRegistry, deployContext); !util.IsTestMode() && err != nil { + logrus.Error(err) + } + } else { + // the empty string for a host is intentional here - we let OpenShift decide on the hostname + route, err := SyncRouteToCluster(deployContext, PluginRegistry, "", PluginRegistry, 8080) + if !util.IsTestMode() { + if route == nil { + logrus.Infof("Waiting on route '%s' to be ready", PluginRegistry) + if err != nil { + logrus.Error(err) + } - if !util.IsTestMode() { - host = route.Spec.Host + return false, err + } + } + if err := DeleteGatewayRouteConfig(pluginRegistryGatewayConfig, deployContext); !util.IsTestMode() && err != nil { + logrus.Error(err) + } + + if !util.IsTestMode() { + endpoint = route.Spec.Host + } } } if pluginRegistryURL == "" { if deployContext.CheCluster.Spec.Server.TlsSupport { - pluginRegistryURL = "https://" + host + "/v3" + pluginRegistryURL = "https://" + endpoint + "/v3" } else { - pluginRegistryURL = "http://" + host + "/v3" + pluginRegistryURL = "http://" + endpoint + "/v3" } } diff --git a/pkg/deploy/route.go b/pkg/deploy/route.go index 69f7b69fc..e1cd4aea5 100644 --- a/pkg/deploy/route.go +++ b/pkg/deploy/route.go @@ -84,6 +84,22 @@ func SyncRouteToCluster( return clusterRoute, err } +func DeleteRouteIfExists(name string, deployContext *DeployContext) error { + ingress, err := GetClusterRoute(name, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client) + if err != nil { + return err + } + + if ingress != nil { + err = deployContext.ClusterAPI.Client.Delete(context.TODO(), ingress) + if err != nil { + return err + } + } + + return nil +} + // GetClusterRoute returns existing route. func GetClusterRoute(name string, namespace string, client runtimeClient.Client) (*routev1.Route, error) { route := &routev1.Route{} diff --git a/pkg/deploy/service.go b/pkg/deploy/service.go index 41ef4bc7e..8aa3479e8 100644 --- a/pkg/deploy/service.go +++ b/pkg/deploy/service.go @@ -33,7 +33,7 @@ type ServiceProvisioningStatus struct { } const ( - CheServiceHame = "che-host" + CheServiceName = "che-host" ) var portsDiffOpts = cmp.Options{ @@ -66,7 +66,7 @@ func GetSpecCheService(deployContext *DeployContext) (*corev1.Service, error) { portNumber = append(portNumber, DefaultCheDebugPort) } - return getSpecService(deployContext, CheServiceHame, portName, portNumber, labels) + return getSpecService(deployContext, CheServiceName, portName, portNumber, labels) } func SyncServiceToCluster( diff --git a/pkg/util/util.go b/pkg/util/util.go index 812cffccb..d5bde78c1 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -27,6 +27,7 @@ import ( "strings" "time" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -138,7 +139,6 @@ func GetServerResources() ([]*v1.APIResourceList, error) { } func GetValue(key string, defaultValue string) (value string) { - value = key if len(key) < 1 { value = defaultValue @@ -146,6 +146,41 @@ func GetValue(key string, defaultValue string) (value string) { return value } +func GetMapValue(value map[string]string, defaultValue map[string]string) map[string]string { + ret := value + if len(value) < 1 { + ret = defaultValue + } + + return ret +} + +func MergeMaps(first map[string]string, second map[string]string) map[string]string { + ret := make(map[string]string) + for k, v := range first { + ret[k] = v + } + + for k, v := range second { + ret[k] = v + } + + return ret +} + +func GetServerExposureStrategy(c *orgv1.CheCluster, defaultValue string) string { + strategy := c.Spec.Server.ServerExposureStrategy + if IsOpenShift { + strategy = GetValue(strategy, defaultValue) + } else { + if strategy == "" { + strategy = GetValue(c.Spec.K8s.IngressStrategy, defaultValue) + } + } + + return strategy +} + func IsTestMode() (isTesting bool) { testMode := os.Getenv("MOCK_API")