Manage the Traefik gateway for implementing single host on OpenShift (#378)

Co-authored-by: Michal Vala <michal.vala@gmail.com>
pull/447/head
Lukas Krejci 2020-09-16 15:21:57 +02:00 committed by GitHub
parent c824447348
commit bda65a4e40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1441 additions and 295 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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: "",
},
},
},
},
},
},
},
},

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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(.*)",

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

582
pkg/deploy/gateway.go Normal file
View File

@ -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),
},
},
},
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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"
}
}

View File

@ -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{}

View File

@ -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(

View File

@ -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")