diff --git a/controllers/che/checluster_controller.go b/controllers/che/checluster_controller.go index 9a4a698b5..26b46cda5 100644 --- a/controllers/che/checluster_controller.go +++ b/controllers/che/checluster_controller.go @@ -145,9 +145,9 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { return []ctrl.Request{} } - var toEclipseCheSecretRequestMapper handler.ToRequestsFunc = func(obj handler.MapObject) []ctrl.Request { - isEclipseCheSecret, reconcileRequest := isEclipseCheSecret(mgr, obj) - if isEclipseCheSecret { + var toEclipseCheRelatedObjRequestMapper handler.ToRequestsFunc = func(obj handler.MapObject) []ctrl.Request { + isEclipseCheRelatedObj, reconcileRequest := isEclipseCheRelatedObj(mgr, obj) + if isEclipseCheRelatedObj { return []ctrl.Request{reconcileRequest} } return []ctrl.Request{} @@ -194,7 +194,11 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { builder.WithPredicates(onAllExceptGenericEventsPredicate), ). Watches(&source.Kind{Type: &corev1.Secret{}}, - &handler.EnqueueRequestsFromMapFunc{ToRequests: toEclipseCheSecretRequestMapper}, + &handler.EnqueueRequestsFromMapFunc{ToRequests: toEclipseCheRelatedObjRequestMapper}, + builder.WithPredicates(onAllExceptGenericEventsPredicate), + ). + Watches(&source.Kind{Type: &corev1.ConfigMap{}}, + &handler.EnqueueRequestsFromMapFunc{ToRequests: toEclipseCheRelatedObjRequestMapper}, builder.WithPredicates(onAllExceptGenericEventsPredicate), ) @@ -727,9 +731,9 @@ func (r *CheClusterReconciler) autoEnableOAuth(deployContext *deploy.DeployConte return reconcile.Result{}, nil } -// isEclipseCheSecret indicates if there is a secret with +// isEclipseCheRelatedObj indicates if there is a object with // the label 'app.kubernetes.io/part-of=che.eclipse.org' in a che namespace -func isEclipseCheSecret(mgr ctrl.Manager, obj handler.MapObject) (bool, ctrl.Request) { +func isEclipseCheRelatedObj(mgr ctrl.Manager, obj handler.MapObject) (bool, ctrl.Request) { checlusters := &orgv1.CheClusterList{} if err := mgr.GetClient().List(context.TODO(), checlusters, &client.ListOptions{}); err != nil { return false, ctrl.Request{} diff --git a/pkg/deploy/deployment.go b/pkg/deploy/deployment.go index edf9176be..350634e16 100644 --- a/pkg/deploy/deployment.go +++ b/pkg/deploy/deployment.go @@ -53,6 +53,10 @@ func SyncDeploymentSpecToCluster( return false, err } + if err := MountConfigMaps(deploymentSpec, deployContext); err != nil { + return false, err + } + done, err := Sync(deployContext, deploymentSpec, deploymentDiffOpts) if err != nil || !done { return false, err @@ -176,3 +180,103 @@ func MountSecrets(specDeployment *appsv1.Deployment, deployContext *DeployContex return nil } + +// MountConfigMaps mounts configmaps into a container as a file or as environment variable. +// Configmaps are selected by the following labels: +// - app.kubernetes.io/part-of=che.eclipse.org +// - app.kubernetes.io/component=-configmap +func MountConfigMaps(specDeployment *appsv1.Deployment, deployContext *DeployContext) error { + configmaps := &corev1.ConfigMapList{} + + kubernetesPartOfLabelSelectorRequirement, _ := labels.NewRequirement(KubernetesPartOfLabelKey, selection.Equals, []string{CheEclipseOrg}) + kubernetesComponentLabelSelectorRequirement, _ := labels.NewRequirement(KubernetesComponentLabelKey, selection.Equals, []string{specDeployment.Name + "-configmap"}) + + listOptions := &client.ListOptions{ + LabelSelector: labels.NewSelector().Add(*kubernetesPartOfLabelSelectorRequirement).Add(*kubernetesComponentLabelSelectorRequirement), + } + if err := deployContext.ClusterAPI.Client.List(context.TODO(), configmaps, listOptions); err != nil { + return err + } + + // sort configmaps by name first to have the same order every time + sort.Slice(configmaps.Items, func(i, j int) bool { + return strings.Compare(configmaps.Items[i].Name, configmaps.Items[j].Name) < 0 + }) + + container := &specDeployment.Spec.Template.Spec.Containers[0] + for _, configMapObj := range configmaps.Items { + switch configMapObj.Annotations[CheEclipseOrgMountAs] { + case "file": + voluseSource := corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMapObj.Name, + }, + }, + } + + volume := corev1.Volume{ + Name: configMapObj.Name, + VolumeSource: voluseSource, + } + + volumeMount := corev1.VolumeMount{ + Name: configMapObj.Name, + MountPath: configMapObj.Annotations[CheEclipseOrgMountPath], + } + + specDeployment.Spec.Template.Spec.Volumes = append(specDeployment.Spec.Template.Spec.Volumes, volume) + container.VolumeMounts = append(container.VolumeMounts, volumeMount) + + case "env": + configmap := &corev1.ConfigMap{} + exists, err := GetNamespacedObject(deployContext, configMapObj.Name, configmap) + if err != nil { + return err + } else if !exists { + return fmt.Errorf("ConfigMap '%s' not found", configMapObj.Name) + } + + // grab all keys and sort first to have the same order every time + keys := make([]string, 0) + for k := range configmap.Data { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return strings.Compare(keys[i], keys[j]) < 0 + }) + + for _, key := range keys { + var envName string + + // check if evn name defined explicitly + envNameAnnotation := CheEclipseOrg + "/" + key + "_env-name" + envName, envNameExists := configMapObj.Annotations[envNameAnnotation] + if !envNameExists { + // check if there is only one env name to mount + envName, envNameExists = configMapObj.Annotations[CheEclipseOrgEnvName] + if len(configmap.Data) > 1 { + return fmt.Errorf("There are more than one environment variable to mount. Use annotation '%s' to specify a name", envNameAnnotation) + } else if !envNameExists { + return fmt.Errorf("Environment name to mount configmap key not found. Use annotation '%s' to specify a name", CheEclipseOrgEnvName) + } + } + + env := corev1.EnvVar{ + Name: envName, + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: key, + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMapObj.Name, + }, + }, + }, + } + container.Env = append(container.Env, env) + } + } + } + + return nil +} diff --git a/pkg/deploy/deployment_test.go b/pkg/deploy/deployment_test.go index 564f2639c..37094cf7a 100644 --- a/pkg/deploy/deployment_test.go +++ b/pkg/deploy/deployment_test.go @@ -325,6 +325,278 @@ func TestMountSecret(t *testing.T) { } } +func TestMountConfigMaps(t *testing.T) { + type testCase struct { + name string + initDeployment *appsv1.Deployment + expectedDeployment *appsv1.Deployment + initObjects []runtime.Object + } + + testCases := []testCase{ + { + name: "Mount configmap as file", + initDeployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{}}, + }, + }, + }, + }, + expectedDeployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "test-volume", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-volume", + }, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + VolumeMounts: []corev1.VolumeMount{ + { + Name: "test-volume", + MountPath: "/test-path", + }, + }, + }, + }, + }, + }, + }, + }, + initObjects: []runtime.Object{ + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-volume", + Namespace: "eclipse-che", + Labels: map[string]string{ + KubernetesPartOfLabelKey: CheEclipseOrg, + KubernetesComponentLabelKey: "che-configmap", // corresponds to deployment name + }, + Annotations: map[string]string{ + CheEclipseOrgMountAs: "file", + CheEclipseOrgMountPath: "/test-path", + }, + }, + Data: map[string]string{ + "key": "key-data", + }, + }, + }, + }, + { + name: "Mount env variable", + initDeployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{}}, + }, + }, + }, + }, + expectedDeployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "ENV_A", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "a", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-envs", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + initObjects: []runtime.Object{ + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-envs", + Namespace: "eclipse-che", + Labels: map[string]string{ + KubernetesPartOfLabelKey: CheEclipseOrg, + KubernetesComponentLabelKey: "che-configmap", // corresponds to deployment name + }, + Annotations: map[string]string{ + CheEclipseOrgMountAs: "env", + CheEclipseOrgEnvName: "ENV_A", + }, + }, + Data: map[string]string{ + "a": "a-data", + }, + }, + }, + }, + { + name: "Mount several env variables", + initDeployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{}}, + }, + }, + }, + }, + expectedDeployment: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "ENV_A", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "a", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-envs", + }, + }, + }, + }, + { + Name: "ENV_B", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "b", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-envs", + }, + }, + }, + }, + { + Name: "ENV_C", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "c", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-envs", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + initObjects: []runtime.Object{ + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-envs", + Namespace: "eclipse-che", + Labels: map[string]string{ + KubernetesPartOfLabelKey: CheEclipseOrg, + KubernetesComponentLabelKey: "che-configmap", // corresponds to deployment name + }, + Annotations: map[string]string{ + CheEclipseOrgMountAs: "env", + CheEclipseOrg + "/a_env-name": "ENV_A", + CheEclipseOrg + "/b_env-name": "ENV_B", + CheEclipseOrg + "/c_env-name": "ENV_C", + }, + }, + Data: map[string]string{ + "b": "a-data", + "a": "b-data", + "c": "c-data", + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + logf.SetLogger(zap.LoggerTo(os.Stdout, true)) + orgv1.SchemeBuilder.AddToScheme(scheme.Scheme) + testCase.initObjects = append(testCase.initObjects, testCase.initDeployment) + cli := fake.NewFakeClientWithScheme(scheme.Scheme, testCase.initObjects...) + + deployContext := &DeployContext{ + CheCluster: &orgv1.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + }, + }, + ClusterAPI: ClusterAPI{ + Client: cli, + NonCachedClient: cli, + Scheme: scheme.Scheme, + }, + } + + err := MountConfigMaps(testCase.initDeployment, deployContext) + if err != nil { + t.Fatalf("Error mounting configmap: %v", err) + } + + if !reflect.DeepEqual(testCase.expectedDeployment, testCase.initDeployment) { + t.Errorf("Expected deployment and deployment returned from API server differ (-want, +got): %v", cmp.Diff(testCase.expectedDeployment, testCase.initDeployment)) + } + }) + } +} + func TestSyncEnvVarDeploymentToCluster(t *testing.T) { orgv1.SchemeBuilder.AddToScheme(scheme.Scheme) cli := fake.NewFakeClientWithScheme(scheme.Scheme) diff --git a/templates/keycloak-provision.sh b/templates/keycloak-provision.sh index 1aa0e0ffc..9911508f1 100644 --- a/templates/keycloak-provision.sh +++ b/templates/keycloak-provision.sh @@ -37,12 +37,24 @@ provisionKeycloak() { -s adminTheme={{ .KeycloakTheme }} \ -s emailTheme={{ .KeycloakTheme }} + DEFAULT_WEBORIGINS='"http://{{ .CheHost }}", "https://{{ .CheHost }}"' + # ADDITIONAL_WEBORIGINS is an env var in format '"url1", "url2"' + # which if specified, is provisioned to keycloak additionally to Che's URLs ones + [ ! -z "$ADDITIONAL_WEBORIGINS" ] && ADDITIONAL_WEBORIGINS=", $ADDITIONAL_WEBORIGINS" + WEBORIGINS="[$DEFAULT_WEBORIGINS $ADDITIONAL_WEBORIGINS]" + + DEFAULT_REDIRECT_URIS='"http://{{ .CheHost }}/dashboard/*", "https://{{ .CheHost }}/dashboard/*", "http://{{ .CheHost }}/factory*", "https://{{ .CheHost }}/factory*", "http://{{ .CheHost }}/f*", "https://{{ .CheHost }}/f*", "http://{{ .CheHost }}/_app/*", "https://{{ .CheHost }}/_app/*", "http://{{ .CheHost }}/swagger/*", "https://{{ .CheHost }}/swagger/*"' + # ADDITIONAL_REDIRECT_URIS is an env var in format '"url1", "url2"' + # which if specified, is provisioned to keycloak additionally to Che's URLs ones + [ ! -z "$ADDITIONAL_REDIRECT_URIS" ] && ADDITIONAL_REDIRECT_URIS=", $ADDITIONAL_REDIRECT_URIS" + REDIRECT_URIS="[$DEFAULT_REDIRECT_URIS $ADDITIONAL_REDIRECT_URIS]" + {{ .Script }} create clients \ -r '{{ .KeycloakRealm }}' \ -s clientId={{ .KeycloakClientId }} \ -s id={{ .KeycloakClientId }} \ - -s webOrigins='["http://{{ .CheHost }}", "https://{{ .CheHost }}"]' \ - -s redirectUris='["http://{{ .CheHost }}/dashboard/*", "https://{{ .CheHost }}/dashboard/*", "http://{{ .CheHost }}/factory*", "https://{{ .CheHost }}/factory*", "http://{{ .CheHost }}/f*", "https://{{ .CheHost }}/f*", "http://{{ .CheHost }}/_app/*", "https://{{ .CheHost }}/_app/*", "http://{{ .CheHost }}/swagger/*", "https://{{ .CheHost }}/swagger/*"]' \ + -s webOrigins="$WEBORIGINS" \ + -s redirectUris="$REDIRECT_URIS" \ -s directAccessGrantsEnabled=true \ -s publicClient=true diff --git a/templates/keycloak-update.sh b/templates/keycloak-update.sh index 7be9dbf54..994a52b36 100644 --- a/templates/keycloak-update.sh +++ b/templates/keycloak-update.sh @@ -15,10 +15,22 @@ connectToKeycloak() { } updateKeycloak() { + DEFAULT_WEBORIGINS='"http://{{ .CheHost }}", "https://{{ .CheHost }}"' + # ADDITIONAL_WEBORIGINS is an env var in format '"url1", "url2"' + # which if specified, is provisioned to keycloak additionally to Che's URLs ones + [ ! -z "$ADDITIONAL_WEBORIGINS" ] && ADDITIONAL_WEBORIGINS=", $ADDITIONAL_WEBORIGINS" + WEBORIGINS="[$DEFAULT_WEBORIGINS $ADDITIONAL_WEBORIGINS]" + + DEFAULT_REDIRECT_URIS='"http://{{ .CheHost }}/dashboard/*", "https://{{ .CheHost }}/dashboard/*", "http://{{ .CheHost }}/factory*", "https://{{ .CheHost }}/factory*", "http://{{ .CheHost }}/f*", "https://{{ .CheHost }}/f*", "http://{{ .CheHost }}/_app/*", "https://{{ .CheHost }}/_app/*", "http://{{ .CheHost }}/swagger/*", "https://{{ .CheHost }}/swagger/*"' + # ADDITIONAL_REDIRECT_URIS is an env var in format '"url1", "url2"' + # which if specified, is provisioned to keycloak additionally to Che's URLs ones + [ ! -z "$ADDITIONAL_REDIRECT_URIS" ] && ADDITIONAL_REDIRECT_URIS=", $ADDITIONAL_REDIRECT_URIS" + REDIRECT_URIS="[$DEFAULT_REDIRECT_URIS $ADDITIONAL_REDIRECT_URIS]" + {{ .Script }} update clients/{{ .KeycloakClientId }} \ -r '{{ .KeycloakRealm }}' \ - -s webOrigins='["http://{{ .CheHost }}", "https://{{ .CheHost }}"]' \ - -s redirectUris='["http://{{ .CheHost }}/dashboard/*", "https://{{ .CheHost }}/dashboard/*", "http://{{ .CheHost }}/factory*", "https://{{ .CheHost }}/factory*", "http://{{ .CheHost }}/f*", "https://{{ .CheHost }}/f*", "http://{{ .CheHost }}/_app/*", "https://{{ .CheHost }}/_app/*", "http://{{ .CheHost }}/swagger/*", "https://{{ .CheHost }}/swagger/*"]' + -s webOrigins="$WEBORIGINS" \ + -s redirectUris="$REDIRECT_URIS" } checkKeycloak() {