diff --git a/controllers/workspaceconfig/workspaces_config_controller_test.go b/controllers/workspaceconfig/workspaces_config_controller_test.go index e18840683..1219e7807 100644 --- a/controllers/workspaceconfig/workspaces_config_controller_test.go +++ b/controllers/workspaceconfig/workspaces_config_controller_test.go @@ -18,8 +18,6 @@ import ( "testing" "github.com/eclipse-che/che-operator/controllers/namespacecache" - "github.com/eclipse-che/che-operator/pkg/common/diffs" - "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/api/errors" rbacv1 "k8s.io/api/rbac/v1" @@ -84,10 +82,9 @@ func TestCreate(t *testing.T) { assert.NoError(t, err) dstCm := &corev1.ConfigMap{} - exists, err := deploy.Get(ctx, types.NamespacedName{Namespace: "user-che", Name: "test"}, dstCm) + err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: "user-che", Name: "test"}, dstCm) assert.NoError(t, err) - assert.True(t, exists) assert.Equal(t, 1, len(dstCm.Data)) assert.Equal(t, "new-value", dstCm.Data["key"]) } @@ -141,7 +138,8 @@ func TestUpdate(t *testing.T) { err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: "test", Namespace: "eclipse-che"}, srcCm) assert.NoError(t, err) - assert.True(t, cmp.Equal(dstCm, srcCm, diffs.ConfigMap([]string{constants.KubernetesPartOfLabelKey, constants.KubernetesComponentLabelKey}, nil))) + assert.Equal(t, srcCm.Labels[constants.KubernetesPartOfLabelKey], dstCm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, srcCm.Labels[constants.KubernetesComponentLabelKey], dstCm.Labels[constants.KubernetesComponentLabelKey]) // update source and destination config maps diff --git a/pkg/common/diffs/diffs.go b/pkg/common/diffs/diffs.go index ffc9d348e..bc8254bb5 100644 --- a/pkg/common/diffs/diffs.go +++ b/pkg/common/diffs/diffs.go @@ -51,10 +51,11 @@ var ConfigMapEnsureLabels = cmp.Options{ }), } -func ConfigMap(labels []string, annotations []string) cmp.Options { +// ConfigMap respects existed labels and annotations +func ConfigMap(labelKeys []string, annotationKeys []string) cmp.Options { return cmp.Options{ cmpopts.IgnoreFields(corev1.ConfigMap{}, "TypeMeta"), - objectMetaComparator(labels, annotations), + cmpMetadata(labelKeys, annotationKeys), } } @@ -62,7 +63,7 @@ var ServiceMonitor = cmp.Options{ cmpopts.IgnoreFields(monitoringv1.ServiceMonitor{}, "TypeMeta", "ObjectMeta"), } -func objectMetaComparator(labels []string, annotations []string) cmp.Option { +func cmpMetadata(labels []string, annotations []string) cmp.Option { return cmp.Comparer(func(x, y metav1.ObjectMeta) bool { for _, label := range labels { if x.Labels[label] != y.Labels[label] { diff --git a/pkg/deploy/devworkspace/dev_workspace_config.go b/pkg/deploy/devworkspace/dev_workspace_config.go index ca464d575..f0b37952f 100644 --- a/pkg/deploy/devworkspace/dev_workspace_config.go +++ b/pkg/deploy/devworkspace/dev_workspace_config.go @@ -13,6 +13,7 @@ package devworkspace import ( + "context" "encoding/json" "fmt" "maps" @@ -26,10 +27,12 @@ import ( "github.com/eclipse-che/che-operator/pkg/common/reconciler" "github.com/eclipse-che/che-operator/pkg/common/utils" "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -123,6 +126,10 @@ func updateWorkspaceConfig(ctx *chetypes.DeployContext, operatorConfig *controll updateInitContainers(devEnvironments, operatorConfig.Workspace) + if err := updateTLSCertificateConfigmapRef(ctx, operatorConfig); err != nil { + return err + } + // If the CheCluster has a configured proxy, or if the Che Operator has detected a proxy configuration, // we need to disable automatic proxy handling in the DevWorkspace Operator as its implementation collides // with ours -- they set environment variables the deployment spec explicitly, which overrides the proxy-settings @@ -301,6 +308,39 @@ func updateInitContainers(devEnvironments *chev2.CheClusterDevEnvironments, work workspaceConfig.InitContainers = devEnvironments.InitContainers } +func updateTLSCertificateConfigmapRef(ctx *chetypes.DeployContext, operatorConfig *controllerv1alpha1.OperatorConfiguration) error { + cm := &corev1.ConfigMap{} + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( + context.TODO(), + types.NamespacedName{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: ctx.CheCluster.Namespace, + }, + cm, + ) + + if err != nil { + return fmt.Errorf("failed to get ConfigMap %s: %w", tls.CheMergedCABundleCertsCMName, err) + } + + if exists && len(cm.Data) > 0 { + if operatorConfig.Routing == nil { + operatorConfig.Routing = &controllerv1alpha1.RoutingConfig{} + } + + operatorConfig.Routing.TLSCertificateConfigmapRef = &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: ctx.CheCluster.Namespace, + } + } else { + if operatorConfig.Routing != nil { + operatorConfig.Routing.TLSCertificateConfigmapRef = nil + } + } + + return nil +} + func disableDWOProxy(routingConfig *controllerv1alpha1.RoutingConfig) { // Since we create proxy configmaps to mount proxy settings, we want to disable // proxy handling in DWO; otherwise the env vars added by DWO will override the env diff --git a/pkg/deploy/devworkspace/dev_workspace_config_routing_test.go b/pkg/deploy/devworkspace/dev_workspace_config_routing_test.go new file mode 100644 index 000000000..19f6346ac --- /dev/null +++ b/pkg/deploy/devworkspace/dev_workspace_config_routing_test.go @@ -0,0 +1,469 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "context" + "testing" + + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestReconcileDevWorkspaceConfigTLSCertificateConfigmapRef(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedRoutingConfig *controllerv1alpha1.RoutingConfig + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with TLSCertificateConfigmapRef when CA bundle ConfigMap exists with data", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + Data: map[string]string{ + "ca-bundle.crt": "certificate-data", + }, + }, + }, + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{ + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig with TLSCertificateConfigmapRef when CA bundle ConfigMap exists with data", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + Data: map[string]string{ + "ca-bundle.crt": "certificate-data", + }, + }, + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Routing: &controllerv1alpha1.RoutingConfig{ + DefaultRoutingClass: "che", + }, + }, + }, + }, + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{ + DefaultRoutingClass: "che", + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + }, + }, + { + name: "Do not set TLSCertificateConfigmapRef when CA bundle ConfigMap does not exist", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + expectedRoutingConfig: nil, + }, + { + name: "Do not set TLSCertificateConfigmapRef when CA bundle ConfigMap exists but has no data", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + Data: map[string]string{}, + }, + }, + expectedRoutingConfig: nil, + }, + { + name: "Clear TLSCertificateConfigmapRef when CA bundle ConfigMap is removed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Routing: &controllerv1alpha1.RoutingConfig{ + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + }, + }, + }, + }, + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{}, + }, + { + name: "Clear TLSCertificateConfigmapRef when CA bundle ConfigMap data is emptied", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + Data: map[string]string{}, + }, + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Routing: &controllerv1alpha1.RoutingConfig{ + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + }, + }, + }, + }, + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{}, + }, + { + name: "Re-reconcile is stable when TLSCertificateConfigmapRef already matches", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + Data: map[string]string{ + "ca-bundle.crt": "certificate-data", + }, + }, + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Routing: &controllerv1alpha1.RoutingConfig{ + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + }, + }, + }, + }, + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{ + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + }, + }, + { + name: "Clear TLSCertificateConfigmapRef while preserving other routing config", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Routing: &controllerv1alpha1.RoutingConfig{ + DefaultRoutingClass: "che", + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + }, + }, + }, + }, + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{ + DefaultRoutingClass: "che", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get( + context.TODO(), + types.NamespacedName{ + Name: devWorkspaceConfigName, + Namespace: testCase.cheCluster.Namespace, + }, + dwoc, + ) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedRoutingConfig, dwoc.Config.Routing) + }) + } +} + +func TestReconcileDevWorkspaceConfigProxyAndTLSComposition(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + httpProxy string + httpsProxy string + expectedRoutingConfig *controllerv1alpha1.RoutingConfig + } + + var testCases = []testCase{ + { + name: "Both proxy and TLS certificate configmap ref are set on RoutingConfig", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + Data: map[string]string{ + "ca-bundle.crt": "certificate-data", + }, + }, + }, + httpProxy: "http://proxy.example.com:3128", + httpsProxy: "https://proxy.example.com:3128", + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{ + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + ProxyConfig: &controllerv1alpha1.Proxy{ + HttpProxy: ptr.To(""), + HttpsProxy: ptr.To(""), + NoProxy: ptr.To(""), + }, + }, + }, + { + name: "Proxy set without TLS certificate configmap does not clobber routing", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + httpProxy: "http://proxy.example.com:3128", + httpsProxy: "https://proxy.example.com:3128", + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{ + ProxyConfig: &controllerv1alpha1.Proxy{ + HttpProxy: ptr.To(""), + HttpsProxy: ptr.To(""), + NoProxy: ptr.To(""), + }, + }, + }, + { + name: "TLS certificate configmap ref set without proxy does not clobber routing", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + Data: map[string]string{ + "ca-bundle.crt": "certificate-data", + }, + }, + }, + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{ + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + }, + }, + { + name: "Both proxy and TLS compose with existing routing config", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + }, + existedObjects: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + Data: map[string]string{ + "ca-bundle.crt": "certificate-data", + }, + }, + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Routing: &controllerv1alpha1.RoutingConfig{ + DefaultRoutingClass: "che", + }, + }, + }, + }, + httpProxy: "http://proxy.example.com:3128", + httpsProxy: "https://proxy.example.com:3128", + expectedRoutingConfig: &controllerv1alpha1.RoutingConfig{ + DefaultRoutingClass: "che", + TLSCertificateConfigmapRef: &controllerv1alpha1.ConfigmapReference{ + Name: tls.CheMergedCABundleCertsCMName, + Namespace: "eclipse-che", + }, + ProxyConfig: &controllerv1alpha1.Proxy{ + HttpProxy: ptr.To(""), + HttpsProxy: ptr.To(""), + NoProxy: ptr.To(""), + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + deployContext.Proxy.HttpProxy = testCase.httpProxy + deployContext.Proxy.HttpsProxy = testCase.httpsProxy + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get( + context.TODO(), + types.NamespacedName{ + Name: devWorkspaceConfigName, + Namespace: testCase.cheCluster.Namespace, + }, + dwoc, + ) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedRoutingConfig, dwoc.Config.Routing) + }) + } +} diff --git a/pkg/deploy/devworkspace/dev_workspace_config_scheduling_test.go b/pkg/deploy/devworkspace/dev_workspace_config_scheduling_test.go new file mode 100644 index 000000000..56511a024 --- /dev/null +++ b/pkg/deploy/devworkspace/dev_workspace_config_scheduling_test.go @@ -0,0 +1,600 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "context" + "testing" + + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestReconcileDevWorkspaceConfigProgressTimeout(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with progressTimeout", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + StartTimeoutSeconds: ptr.To(int32(600)), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ProgressTimeout: "600s", + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig by adding progressTimeout", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + StartTimeoutSeconds: ptr.To(int32(600)), + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ProgressTimeout: "600s", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig by overwriting progressTimeout", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + StartTimeoutSeconds: ptr.To(int32(420)), + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ProgressTimeout: "1h30m", + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ProgressTimeout: "420s", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig by removing progressTimeout", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ProgressTimeout: "1h30m", + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + + diff := cmp.Diff(testCase.expectedOperatorConfig, dwoc.Config, + cmp.Options{ + cmpopts.IgnoreFields(controllerv1alpha1.WorkspaceConfig{}, "ServiceAccount", "DefaultStorageSize", "StorageClassName", "ProjectCloneConfig", "DeploymentStrategy"), + cmpopts.IgnoreFields(controllerv1alpha1.RoutingConfig{}, "TLSCertificateConfigmapRef"), + }) + assert.Empty(t, diff) + }) + } +} + +func TestReconcileDevWorkspaceConfigPodSchedulerName(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with podSchedulerName", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PodSchedulerName: "test-scheduler", + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + SchedulerName: "test-scheduler", + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when PodSchedulerName is added", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PodSchedulerName: "test-scheduler", + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + SchedulerName: "test-scheduler", + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when PodSchedulerName is changed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PodSchedulerName: "test-scheduler", + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + SchedulerName: "previous-scheduler", + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + SchedulerName: "test-scheduler", + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when PodSchedulerName is removed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PodSchedulerName: "", + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + SchedulerName: "previous-scheduler", + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + DeploymentStrategy: "Recreate", + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.SchedulerName, dwoc.Config.Workspace.SchedulerName) + }) + } +} + +func TestReconcileDevWorkspaceConfigRuntimeClassName(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with RuntimeClassName", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + RuntimeClassName: ptr.To("test-runtime-class"), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + RuntimeClassName: ptr.To("test-runtime-class"), + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when RuntimeClassName is added", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + RuntimeClassName: ptr.To("test-runtime-class"), + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + RuntimeClassName: ptr.To("test-runtime-class"), + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when RuntimeClassName is changed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + RuntimeClassName: ptr.To("test-runtime-class"), + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + RuntimeClassName: ptr.To("previous-runtime-class"), + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + RuntimeClassName: ptr.To("test-runtime-class"), + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when RuntimeClassName is removed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + RuntimeClassName: nil, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + RuntimeClassName: ptr.To("previous-runtime-class"), + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.RuntimeClassName, dwoc.Config.Workspace.RuntimeClassName) + }) + } +} + +func TestReconcileDevWorkspaceConfigDeploymentStrategy(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with DeploymentStrategy", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DeploymentStrategy: "Recreate", + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when DeploymentStrategy is added", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DeploymentStrategy: "Recreate", + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when DeploymentStrategy is changed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DeploymentStrategy: "RollingUpdate", + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + DeploymentStrategy: "Recreate", + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + DeploymentStrategy: "RollingUpdate", + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.DeploymentStrategy, dwoc.Config.Workspace.DeploymentStrategy) + }) + } +} diff --git a/pkg/deploy/devworkspace/dev_workspace_config_security_test.go b/pkg/deploy/devworkspace/dev_workspace_config_security_test.go new file mode 100644 index 000000000..15a1c5357 --- /dev/null +++ b/pkg/deploy/devworkspace/dev_workspace_config_security_test.go @@ -0,0 +1,743 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "context" + "fmt" + "sort" + "testing" + + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestReconcileDevWorkspaceConfigForContainerCapabilities(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var unmasked = corev1.UnmaskedProcMount + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig without Container Security Context if all capabilities disabled", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + DisableContainerRunCapabilities: ptr.To(true), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with Container Security Context if container build enabled", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(false), + DisableContainerRunCapabilities: ptr.To(true), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SETGID", + "SETUID", + }, + }, + AllowPrivilegeEscalation: ptr.To(true), + }, + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with Container Security Context if container run enabled", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + WorkspacesPodAnnotations: map[string]string{ + "annotation_1": "value_1", + "annotation_2": "value_1", + }, + DisableContainerBuildCapabilities: ptr.To(true), + DisableContainerRunCapabilities: ptr.To(false), + ContainerRunConfiguration: &chev2.ContainerRunConfiguration{ + OpenShiftSecurityContextConstraint: "container-run", + WorkspacesPodAnnotations: map[string]string{ + "annotation_1": "value_2", + "annotation_3": "value_2", + }, + ContainerSecurityContext: &corev1.SecurityContext{ + ProcMount: &unmasked, + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SETUID", "SETGID"}, + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + HostUsers: ptr.To(false), + PodAnnotations: map[string]string{ + "annotation_1": "value_2", + "annotation_2": "value_1", + "annotation_3": "value_2", + }, + ContainerSecurityContext: &corev1.SecurityContext{ + ProcMount: &unmasked, + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SETUID", "SETGID"}, + }, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig by adding Container Security Context", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(false), + DisableContainerRunCapabilities: ptr.To(true), + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SETGID", + "SETUID", + }, + }, + AllowPrivilegeEscalation: ptr.To(true), + }, + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig by removing Container Security Context", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + DisableContainerRunCapabilities: ptr.To(true), + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerSecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SETGID", + "SETUID", + }, + }, + AllowPrivilegeEscalation: ptr.To(true), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with Container Security Context if all capabilities enabled", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + WorkspacesPodAnnotations: map[string]string{ + "annotation_1": "value_1", + "annotation_2": "value_1", + }, + DisableContainerBuildCapabilities: ptr.To(false), + DisableContainerRunCapabilities: ptr.To(false), + ContainerRunConfiguration: &chev2.ContainerRunConfiguration{ + OpenShiftSecurityContextConstraint: "container-run", + WorkspacesPodAnnotations: map[string]string{ + "annotation_1": "value_2", + "annotation_3": "value_2", + }, + ContainerSecurityContext: &corev1.SecurityContext{ + ProcMount: &unmasked, + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SETUID", "SETGID"}, + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + HostUsers: ptr.To(false), + PodAnnotations: map[string]string{ + "annotation_1": "value_2", + "annotation_2": "value_1", + "annotation_3": "value_2", + }, + ContainerSecurityContext: &corev1.SecurityContext{ + ProcMount: &unmasked, + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{"SETUID", "SETGID"}, + }, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + + diff := cmp.Diff(testCase.expectedOperatorConfig, dwoc.Config, + cmp.Options{ + cmpopts.IgnoreFields(controllerv1alpha1.WorkspaceConfig{}, "ServiceAccount", "ProjectCloneConfig", "DeploymentStrategy", "DefaultStorageSize", "StorageClassName"), + cmpopts.IgnoreFields(controllerv1alpha1.RoutingConfig{}, "TLSCertificateConfigmapRef"), + }) + assert.Empty(t, diff) + }) + } +} + +func TestReconcileDevWorkspaceContainerSecurityContext(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with Container security context", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + // We disable container build capabilities so that it does not override the container security context we configured + DisableContainerBuildCapabilities: ptr.To(true), + DisableContainerRunCapabilities: ptr.To(true), + Security: chev2.WorkspaceSecurityConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_TIME", + "SETGID", + "SETUID", + }, + Drop: []corev1.Capability{ + "CHOWN", + "KILL", + }, + }, + AllowPrivilegeEscalation: ptr.To(false), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_TIME", + "SETGID", + "SETUID", + }, + Drop: []corev1.Capability{ + "CHOWN", + "KILL", + }, + }, + AllowPrivilegeEscalation: ptr.To(false), + }, + }, + }, + }, + { + name: "Updates existing DevWorkspaceOperatorConfig when Container security context is added", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + DisableContainerRunCapabilities: ptr.To(true), + Security: chev2.WorkspaceSecurityConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_TIME", + "SETGID", + "SETUID", + }, + Drop: []corev1.Capability{ + "CHOWN", + "KILL", + }, + }, + AllowPrivilegeEscalation: ptr.To(false), + }}, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_TIME", + "SETGID", + "SETUID", + }, + Drop: []corev1.Capability{ + "CHOWN", + "KILL", + }, + }, + AllowPrivilegeEscalation: ptr.To(false), + }, + }, + }, + }, + { + name: "Updates existing DevWorkspaceOperatorConfig when Container security is changed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + DisableContainerRunCapabilities: ptr.To(true), + Security: chev2.WorkspaceSecurityConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_TIME", + "SETGID", + "SETUID", + }, + Drop: []corev1.Capability{ + "CHOWN", + "KILL", + }, + }, + AllowPrivilegeEscalation: ptr.To(false), + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "KILL", + }, + Drop: []corev1.Capability{ + "SYS_TIME", + }, + }, + AllowPrivilegeEscalation: ptr.To(true), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "SYS_TIME", + "SETGID", + "SETUID", + }, + Drop: []corev1.Capability{ + "CHOWN", + "KILL", + }, + }, + AllowPrivilegeEscalation: ptr.To(false), + }, + }, + }, + }, + { + name: "Updates existing DevWorkspaceOperatorConfig when Container security is removed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + DisableContainerRunCapabilities: ptr.To(true), + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerSecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "KILL", + }, + Drop: []corev1.Capability{ + "SYS_TIME", + }, + }, + AllowPrivilegeEscalation: ptr.To(true), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + + sortCapabilities := func(capabilities []corev1.Capability) func(i, j int) bool { + return func(i, j int) bool { + return capabilities[i] > capabilities[j] + } + } + expectedContainerSecurityContext := testCase.expectedOperatorConfig.Workspace.ContainerSecurityContext + actualContainerSecurityContext := dwoc.Config.Workspace.ContainerSecurityContext + if expectedContainerSecurityContext != nil { + sort.Slice(expectedContainerSecurityContext.Capabilities.Add, sortCapabilities(expectedContainerSecurityContext.Capabilities.Add)) + sort.Slice(expectedContainerSecurityContext.Capabilities.Drop, sortCapabilities(expectedContainerSecurityContext.Capabilities.Drop)) + } + if actualContainerSecurityContext != nil { + sort.Slice(actualContainerSecurityContext.Capabilities.Add, sortCapabilities(actualContainerSecurityContext.Capabilities.Add)) + sort.Slice(actualContainerSecurityContext.Capabilities.Drop, sortCapabilities(actualContainerSecurityContext.Capabilities.Drop)) + + } + + assert.Equal(t, expectedContainerSecurityContext, actualContainerSecurityContext, + fmt.Sprintf("Did not get expected ContainerSecurityContext.\nDiff:%s", cmp.Diff(expectedContainerSecurityContext, actualContainerSecurityContext))) + }) + } +} + +func TestReconcileDevWorkspacePodSecurityContext(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + configuredPodSecurityContext := &corev1.PodSecurityContext{ + RunAsUser: ptr.To(int64(0)), + RunAsGroup: ptr.To(int64(0)), + RunAsNonRoot: ptr.To(false), + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with Pod security context", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + Security: chev2.WorkspaceSecurityConfig{ + PodSecurityContext: configuredPodSecurityContext, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodSecurityContext: configuredPodSecurityContext, + }, + }, + }, + { + name: "Updates existing DevWorkspaceOperatorConfig with Pod security context", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + Security: chev2.WorkspaceSecurityConfig{ + PodSecurityContext: configuredPodSecurityContext, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodSecurityContext: configuredPodSecurityContext, + }, + }, + }, + { + name: "Updates existing DevWorkspaceOperatorConfig when Pod security context is changed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + Security: chev2.WorkspaceSecurityConfig{ + PodSecurityContext: configuredPodSecurityContext, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodSecurityContext: &corev1.PodSecurityContext{ + RunAsUser: ptr.To(int64(1000)), + RunAsGroup: ptr.To(int64(10001)), + RunAsNonRoot: ptr.To(true), + SupplementalGroups: []int64{ + 5, + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodSecurityContext: configuredPodSecurityContext, + }, + }, + }, + { + name: "Updates existing DevWorkspaceOperatorConfig when Pod security context is removed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{}, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodSecurityContext: &corev1.PodSecurityContext{ + RunAsUser: ptr.To(int64(1000)), + RunAsGroup: ptr.To(int64(10001)), + RunAsNonRoot: ptr.To(true), + SupplementalGroups: []int64{ + 5, + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.PodSecurityContext, dwoc.Config.Workspace.PodSecurityContext, + fmt.Sprintf("Did not get expected PodSecurityContext.\nDiff:%s", cmp.Diff(testCase.expectedOperatorConfig.Workspace.PodSecurityContext, dwoc.Config.Workspace.PodSecurityContext))) + }) + } +} diff --git a/pkg/deploy/devworkspace/dev_workspace_config_service_account_test.go b/pkg/deploy/devworkspace/dev_workspace_config_service_account_test.go new file mode 100644 index 000000000..a8d193f70 --- /dev/null +++ b/pkg/deploy/devworkspace/dev_workspace_config_service_account_test.go @@ -0,0 +1,395 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "context" + "testing" + + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestReconcileServiceAccountConfig(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Case #1", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + ServiceAccount: "service-account", + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + ServiceAccountName: "service-account", + DisableCreation: ptr.To(false), + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Case #2", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DefaultNamespace: chev2.DefaultNamespace{ + AutoProvision: ptr.To(false), + }, + ServiceAccount: "service-account", + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + ServiceAccountName: "service-account", + DisableCreation: ptr.To(true), + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Case #3", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{}, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + DisableCreation: ptr.To(false), + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Case #4", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DefaultNamespace: chev2.DefaultNamespace{ + AutoProvision: ptr.To(false), + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + DisableCreation: ptr.To(false), + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.ServiceAccount, dwoc.Config.Workspace.ServiceAccount) + }) + } +} + +func TestReconcileDevWorkspaceConfigServiceAccountTokens(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with ServiceAccountTokens", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ + { + Name: "test-token-1", + MountPath: "/var/run/secrets/tokens", + Audience: "openshift", + ExpirationSeconds: 3600, + }, + { + Name: "test-token-2", + MountPath: "/var/run/secrets/tokens", + Audience: "kubernetes", + ExpirationSeconds: 180, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ + { + Name: "test-token-1", + MountPath: "/var/run/secrets/tokens", + Audience: "openshift", + ExpirationSeconds: 3600, + }, + { + Name: "test-token-2", + MountPath: "/var/run/secrets/tokens", + Audience: "kubernetes", + ExpirationSeconds: 180, + }, + }}, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when ServiceAccountTokens are added", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ + { + Name: "test-token", + MountPath: "/var/run/secrets/tokens", + Audience: "openshift", + ExpirationSeconds: 3600, + }, + { + Name: "test-token-2", + MountPath: "/var/run/secrets/tokens", + Audience: "kubernetes", + ExpirationSeconds: 180, + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ + { + Name: "test-token", + MountPath: "/var/run/secrets/tokens", + Audience: "openshift", + ExpirationSeconds: 3600, + }, + { + Name: "test-token-2", + MountPath: "/var/run/secrets/tokens", + Audience: "kubernetes", + ExpirationSeconds: 180, + }, + }, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when ServiceAccountTokens are changed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ + { + Name: "new-token", + MountPath: "/var/run/secrets/tokens", + Audience: "openshift", + ExpirationSeconds: 3600, + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ + { + Name: "old-token", + MountPath: "/var/run/secrets/tokens", + Audience: "openshift", + ExpirationSeconds: 60, + }, + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ + { + Name: "new-token", + MountPath: "/var/run/secrets/tokens", + Audience: "openshift", + ExpirationSeconds: 3600, + }, + }, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig when ServiceAccountTokens are removed", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{}, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ + ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ + { + Name: "test-token", + MountPath: "/var/run/secrets/tokens", + Audience: "openshift", + ExpirationSeconds: 3600, + }, + { + Name: "test-token-2", + MountPath: "/var/run/secrets/tokens", + Audience: "kubernetes", + ExpirationSeconds: 180, + }, + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{}, + DeploymentStrategy: "Recreate", + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.ServiceAccount.ServiceAccountTokens, dwoc.Config.Workspace.ServiceAccount.ServiceAccountTokens) + }) + } +} diff --git a/pkg/deploy/devworkspace/dev_workspace_config_storage_test.go b/pkg/deploy/devworkspace/dev_workspace_config_storage_test.go new file mode 100644 index 000000000..f70b2f25c --- /dev/null +++ b/pkg/deploy/devworkspace/dev_workspace_config_storage_test.go @@ -0,0 +1,750 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "context" + "regexp" + "testing" + + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestReconcileDevWorkspaceConfigStorage(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + type errorTestCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedErrorMessage string + } + + var quantity15Gi = resource.MustParse("15Gi") + var quantity10Gi = resource.MustParse("10Gi") + + var expectedErrorTestCases = []errorTestCase{ + { + name: "Invalid claim size string", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerUserPVCStorageStrategy, + PerUserStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + ClaimSize: "invalid-ClaimSize", + }, + }, + }, + }, + }, + expectedErrorMessage: "quantities must match the regular expression", + }, + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{Workspace: &controllerv1alpha1.WorkspaceConfig{DeploymentStrategy: "Recreate"}}, + }, + { + name: "Create DevWorkspaceOperatorConfig with ephemeral storage strategy", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.EphemeralPVCStorageStrategy, + PerUserStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + ClaimSize: "10Gi", + }, + PerWorkspaceStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + ClaimSize: "10Gi", + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{Workspace: &controllerv1alpha1.WorkspaceConfig{DeploymentStrategy: "Recreate"}}, + }, + { + name: "Create DevWorkspaceOperatorConfig with StorageClassName only", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerUserPVCStorageStrategy, + PerUserStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageClassName: ptr.To("test-storage"), + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with non empty StorageAccessMode", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerUserPVCStorageStrategy, + PerUserStrategyPvcConfig: &chev2.PVC{ + StorageAccessMode: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteMany, + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageAccessMode: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteMany, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Not setting PerUserStrategyPvcConfig should reset DevWorkspaceConfig to default StorageAccessMode", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerUserPVCStorageStrategy, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with nil StorageAccessMode", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerUserPVCStorageStrategy, + PerUserStrategyPvcConfig: &chev2.PVC{ + StorageAccessMode: nil, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageAccessMode: nil, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with non empty default container resources", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + DefaultContainerResources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + DefaultContainerResources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with nil default container resources", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + DefaultContainerResources: nil, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + DefaultContainerResources: nil, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with non empty container resource caps", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + ContainerResourceCaps: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + corev1.ResourceCPU: resource.MustParse("2000m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + corev1.ResourceCPU: resource.MustParse("1000m"), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerResourceCaps: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + corev1.ResourceCPU: resource.MustParse("2000m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + corev1.ResourceCPU: resource.MustParse("1000m"), + }, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with nil container resource caps", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + ContainerResourceCaps: nil, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerResourceCaps: nil, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig by adding container resource caps", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + ContainerResourceCaps: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + corev1.ResourceCPU: resource.MustParse("2000m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + corev1.ResourceCPU: resource.MustParse("1000m"), + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerResourceCaps: nil, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerResourceCaps: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + corev1.ResourceCPU: resource.MustParse("2000m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + corev1.ResourceCPU: resource.MustParse("1000m"), + }, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig by changing container resource caps", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + ContainerResourceCaps: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("4Gi"), + corev1.ResourceCPU: resource.MustParse("4000m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + corev1.ResourceCPU: resource.MustParse("2000m"), + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerResourceCaps: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + corev1.ResourceCPU: resource.MustParse("2000m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + corev1.ResourceCPU: resource.MustParse("1000m"), + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerResourceCaps: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("4Gi"), + corev1.ResourceCPU: resource.MustParse("4000m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + corev1.ResourceCPU: resource.MustParse("2000m"), + }, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig by removing container resource caps", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + ContainerResourceCaps: nil, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerResourceCaps: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + corev1.ResourceCPU: resource.MustParse("2000m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + corev1.ResourceCPU: resource.MustParse("1000m"), + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ContainerResourceCaps: nil, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with per-user storage strategy", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerUserPVCStorageStrategy, + PerUserStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + ClaimSize: "15Gi", + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageClassName: ptr.To("test-storage"), + DefaultStorageSize: &controllerv1alpha1.StorageSizes{ + Common: &quantity15Gi, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig with per-workspace storage strategy", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerWorkspacePVCStorageStrategy, + PerWorkspaceStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + ClaimSize: "15Gi", + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageClassName: ptr.To("test-storage"), + DefaultStorageSize: &controllerv1alpha1.StorageSizes{ + PerWorkspace: &quantity15Gi, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update DevWorkspaceOperatorConfig with per-workspace storage strategy", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerWorkspacePVCStorageStrategy, + PerWorkspaceStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + ClaimSize: "15Gi", + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageClassName: ptr.To("default-storage-class"), + DefaultStorageSize: &controllerv1alpha1.StorageSizes{ + PerWorkspace: &quantity10Gi, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageClassName: ptr.To("test-storage"), + DefaultStorageSize: &controllerv1alpha1.StorageSizes{ + PerWorkspace: &quantity15Gi, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update DevWorkspaceOperatorConfig with per-user storage strategy", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerUserPVCStorageStrategy, + PerUserStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + ClaimSize: "15Gi", + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageClassName: ptr.To("default-storage-class"), + DefaultStorageSize: &controllerv1alpha1.StorageSizes{ + Common: &quantity10Gi, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageClassName: ptr.To("test-storage"), + DefaultStorageSize: &controllerv1alpha1.StorageSizes{ + Common: &quantity15Gi, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + { + name: "Update populated DevWorkspaceOperatorConfig with storage class name, storage strategy and storage size", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + DisableContainerBuildCapabilities: ptr.To(true), + Storage: chev2.WorkspaceStorage{ + PvcStrategy: constants.PerUserPVCStorageStrategy, + PerUserStrategyPvcConfig: &chev2.PVC{ + StorageClass: "test-storage", + ClaimSize: "15Gi", + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Routing: &controllerv1alpha1.RoutingConfig{ + DefaultRoutingClass: "routing-class", + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Routing: &controllerv1alpha1.RoutingConfig{ + DefaultRoutingClass: "routing-class", + }, + Workspace: &controllerv1alpha1.WorkspaceConfig{ + StorageClassName: ptr.To("test-storage"), + DefaultStorageSize: &controllerv1alpha1.StorageSizes{ + Common: &quantity15Gi, + }, + DeploymentStrategy: "Recreate", + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + + diff := cmp.Diff(testCase.expectedOperatorConfig, dwoc.Config, cmp.Options{ + cmpopts.IgnoreFields(controllerv1alpha1.WorkspaceConfig{}, "ServiceAccount"), + cmpopts.IgnoreFields(controllerv1alpha1.RoutingConfig{}, "TLSCertificateConfigmapRef"), + }) + assert.Empty(t, diff) + }) + } + + for _, testCase := range expectedErrorTestCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + _, _, err := devWorkspaceConfigReconciler.Reconcile(deployContext) + + assert.Error(t, err) + assert.Regexp(t, regexp.MustCompile(testCase.expectedErrorMessage), err.Error(), "error message must match") + }) + } +} diff --git a/pkg/deploy/devworkspace/dev_workspace_config_test.go b/pkg/deploy/devworkspace/dev_workspace_config_test.go deleted file mode 100644 index 84e6f1d12..000000000 --- a/pkg/deploy/devworkspace/dev_workspace_config_test.go +++ /dev/null @@ -1,3328 +0,0 @@ -// -// Copyright (c) 2019-2025 Red Hat, Inc. -// This program and the accompanying materials are made -// available under the terms of the Eclipse Public License 2.0 -// which is available at https://www.eclipse.org/legal/epl-2.0/ -// -// SPDX-License-Identifier: EPL-2.0 -// -// Contributors: -// Red Hat, Inc. - initial API and implementation -// - -package devworkspace - -import ( - "context" - "fmt" - "regexp" - "sort" - "testing" - - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - - controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" - chev2 "github.com/eclipse-che/che-operator/api/v2" - "github.com/eclipse-che/che-operator/pkg/common/constants" - "github.com/eclipse-che/che-operator/pkg/common/test" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" -) - -func TestReconcileDevWorkspaceConfigStorage(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - type errorTestCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedErrorMessage string - } - - var quantity15Gi = resource.MustParse("15Gi") - var quantity10Gi = resource.MustParse("10Gi") - - var expectedErrorTestCases = []errorTestCase{ - { - name: "Invalid claim size string", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerUserPVCStorageStrategy, - PerUserStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - ClaimSize: "invalid-ClaimSize", - }, - }, - }, - }, - }, - expectedErrorMessage: "quantities must match the regular expression", - }, - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{Workspace: &controllerv1alpha1.WorkspaceConfig{DeploymentStrategy: "Recreate"}}, - }, - { - name: "Create DevWorkspaceOperatorConfig with ephemeral storage strategy", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.EphemeralPVCStorageStrategy, - PerUserStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - ClaimSize: "10Gi", - }, - PerWorkspaceStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - ClaimSize: "10Gi", - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{Workspace: &controllerv1alpha1.WorkspaceConfig{DeploymentStrategy: "Recreate"}}, - }, - { - name: "Create DevWorkspaceOperatorConfig with StorageClassName only", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerUserPVCStorageStrategy, - PerUserStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageClassName: ptr.To("test-storage"), - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with non empty StorageAccessMode", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerUserPVCStorageStrategy, - PerUserStrategyPvcConfig: &chev2.PVC{ - StorageAccessMode: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteMany, - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageAccessMode: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteMany, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Not setting PerUserStrategyPvcConfig should reset DevWorkspaceConfig to default StorageAccessMode", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerUserPVCStorageStrategy, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with nil StorageAccessMode", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerUserPVCStorageStrategy, - PerUserStrategyPvcConfig: &chev2.PVC{ - StorageAccessMode: nil, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageAccessMode: nil, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with non empty default container resources", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - DefaultContainerResources: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("128Mi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("64Mi"), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - DefaultContainerResources: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("128Mi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("64Mi"), - }, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with nil default container resources", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - DefaultContainerResources: nil, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - DefaultContainerResources: nil, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with non empty container resource caps", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - ContainerResourceCaps: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceCPU: resource.MustParse("2000m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - corev1.ResourceCPU: resource.MustParse("1000m"), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerResourceCaps: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceCPU: resource.MustParse("2000m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - corev1.ResourceCPU: resource.MustParse("1000m"), - }, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with nil container resource caps", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - ContainerResourceCaps: nil, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerResourceCaps: nil, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig by adding container resource caps", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - ContainerResourceCaps: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceCPU: resource.MustParse("2000m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - corev1.ResourceCPU: resource.MustParse("1000m"), - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerResourceCaps: nil, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerResourceCaps: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceCPU: resource.MustParse("2000m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - corev1.ResourceCPU: resource.MustParse("1000m"), - }, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig by changing container resource caps", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - ContainerResourceCaps: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("4Gi"), - corev1.ResourceCPU: resource.MustParse("4000m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceCPU: resource.MustParse("2000m"), - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerResourceCaps: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceCPU: resource.MustParse("2000m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - corev1.ResourceCPU: resource.MustParse("1000m"), - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerResourceCaps: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("4Gi"), - corev1.ResourceCPU: resource.MustParse("4000m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceCPU: resource.MustParse("2000m"), - }, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig by removing container resource caps", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - ContainerResourceCaps: nil, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerResourceCaps: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("2Gi"), - corev1.ResourceCPU: resource.MustParse("2000m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi"), - corev1.ResourceCPU: resource.MustParse("1000m"), - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerResourceCaps: nil, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with per-user storage strategy", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerUserPVCStorageStrategy, - PerUserStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - ClaimSize: "15Gi", - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageClassName: ptr.To("test-storage"), - DefaultStorageSize: &controllerv1alpha1.StorageSizes{ - Common: &quantity15Gi, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with per-workspace storage strategy", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerWorkspacePVCStorageStrategy, - PerWorkspaceStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - ClaimSize: "15Gi", - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageClassName: ptr.To("test-storage"), - DefaultStorageSize: &controllerv1alpha1.StorageSizes{ - PerWorkspace: &quantity15Gi, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update DevWorkspaceOperatorConfig with per-workspace storage strategy", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerWorkspacePVCStorageStrategy, - PerWorkspaceStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - ClaimSize: "15Gi", - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageClassName: ptr.To("default-storage-class"), - DefaultStorageSize: &controllerv1alpha1.StorageSizes{ - PerWorkspace: &quantity10Gi, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageClassName: ptr.To("test-storage"), - DefaultStorageSize: &controllerv1alpha1.StorageSizes{ - PerWorkspace: &quantity15Gi, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update DevWorkspaceOperatorConfig with per-user storage strategy", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerUserPVCStorageStrategy, - PerUserStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - ClaimSize: "15Gi", - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageClassName: ptr.To("default-storage-class"), - DefaultStorageSize: &controllerv1alpha1.StorageSizes{ - Common: &quantity10Gi, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageClassName: ptr.To("test-storage"), - DefaultStorageSize: &controllerv1alpha1.StorageSizes{ - Common: &quantity15Gi, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update populated DevWorkspaceOperatorConfig with storage class name, storage strategy and storage size", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - Storage: chev2.WorkspaceStorage{ - PvcStrategy: constants.PerUserPVCStorageStrategy, - PerUserStrategyPvcConfig: &chev2.PVC{ - StorageClass: "test-storage", - ClaimSize: "15Gi", - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Routing: &controllerv1alpha1.RoutingConfig{ - DefaultRoutingClass: "routing-class", - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Routing: &controllerv1alpha1.RoutingConfig{ - DefaultRoutingClass: "routing-class", - }, - Workspace: &controllerv1alpha1.WorkspaceConfig{ - StorageClassName: ptr.To("test-storage"), - DefaultStorageSize: &controllerv1alpha1.StorageSizes{ - Common: &quantity15Gi, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - - diff := cmp.Diff(testCase.expectedOperatorConfig, dwoc.Config, cmp.Options{cmpopts.IgnoreFields(controllerv1alpha1.WorkspaceConfig{}, "ServiceAccount")}) - assert.Empty(t, diff) - }) - } - - for _, testCase := range expectedErrorTestCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - _, _, err := devWorkspaceConfigReconciler.Reconcile(deployContext) - assert.Error(t, err) - assert.Regexp(t, regexp.MustCompile(testCase.expectedErrorMessage), err.Error(), "error message must match") - }) - } -} - -func TestReconcileDevWorkspaceConfigForContainerCapabilities(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var unmasked = corev1.UnmaskedProcMount - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig without Container Security Context if all capabilities disabled", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - DisableContainerRunCapabilities: ptr.To(true), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with Container Security Context if container build enabled", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(false), - DisableContainerRunCapabilities: ptr.To(true), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SETGID", - "SETUID", - }, - }, - AllowPrivilegeEscalation: ptr.To(true), - }, - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with Container Security Context if container run enabled", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - WorkspacesPodAnnotations: map[string]string{ - "annotation_1": "value_1", - "annotation_2": "value_1", - }, - DisableContainerBuildCapabilities: ptr.To(true), - DisableContainerRunCapabilities: ptr.To(false), - ContainerRunConfiguration: &chev2.ContainerRunConfiguration{ - OpenShiftSecurityContextConstraint: "container-run", - WorkspacesPodAnnotations: map[string]string{ - "annotation_1": "value_2", - "annotation_3": "value_2", - }, - ContainerSecurityContext: &corev1.SecurityContext{ - ProcMount: &unmasked, - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"SETUID", "SETGID"}, - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - HostUsers: ptr.To(false), - PodAnnotations: map[string]string{ - "annotation_1": "value_2", - "annotation_2": "value_1", - "annotation_3": "value_2", - }, - ContainerSecurityContext: &corev1.SecurityContext{ - ProcMount: &unmasked, - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"SETUID", "SETGID"}, - }, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig by adding Container Security Context", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(false), - DisableContainerRunCapabilities: ptr.To(true), - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SETGID", - "SETUID", - }, - }, - AllowPrivilegeEscalation: ptr.To(true), - }, - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig by removing Container Security Context", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - DisableContainerRunCapabilities: ptr.To(true), - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerSecurityContext: &corev1.SecurityContext{Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SETGID", - "SETUID", - }, - }, - AllowPrivilegeEscalation: ptr.To(true), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig with Container Security Context if all capabilities enabled", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - WorkspacesPodAnnotations: map[string]string{ - "annotation_1": "value_1", - "annotation_2": "value_1", - }, - DisableContainerBuildCapabilities: ptr.To(false), - DisableContainerRunCapabilities: ptr.To(false), - ContainerRunConfiguration: &chev2.ContainerRunConfiguration{ - OpenShiftSecurityContextConstraint: "container-run", - WorkspacesPodAnnotations: map[string]string{ - "annotation_1": "value_2", - "annotation_3": "value_2", - }, - ContainerSecurityContext: &corev1.SecurityContext{ - ProcMount: &unmasked, - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"SETUID", "SETGID"}, - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - HostUsers: ptr.To(false), - PodAnnotations: map[string]string{ - "annotation_1": "value_2", - "annotation_2": "value_1", - "annotation_3": "value_2", - }, - ContainerSecurityContext: &corev1.SecurityContext{ - ProcMount: &unmasked, - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"SETUID", "SETGID"}, - }, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - - diff := cmp.Diff(testCase.expectedOperatorConfig, dwoc.Config, - cmp.Options{cmpopts.IgnoreFields(controllerv1alpha1.WorkspaceConfig{}, "ServiceAccount", "ProjectCloneConfig", "DeploymentStrategy", "DefaultStorageSize", "StorageClassName")}) - assert.Empty(t, diff) - }) - } -} - -func TestReconcileDevWorkspaceConfigProgressTimeout(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig with progressTimeout", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - StartTimeoutSeconds: ptr.To(int32(600)), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ProgressTimeout: "600s", - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig by adding progressTimeout", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: "eclipse-che", - Namespace: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - StartTimeoutSeconds: ptr.To(int32(600)), - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ProgressTimeout: "600s", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig by overwriting progressTimeout", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - StartTimeoutSeconds: ptr.To(int32(420)), - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ProgressTimeout: "1h30m", - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ProgressTimeout: "420s", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig by removing progressTimeout", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ProgressTimeout: "1h30m", - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - - diff := cmp.Diff(testCase.expectedOperatorConfig, dwoc.Config, - cmp.Options{cmpopts.IgnoreFields(controllerv1alpha1.WorkspaceConfig{}, "ServiceAccount", "DefaultStorageSize", "StorageClassName", "ProjectCloneConfig", "DeploymentStrategy")}) - assert.Empty(t, diff) - }) - } -} - -func TestReconcileServiceAccountConfig(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Case #1", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - ServiceAccount: "service-account", - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - ServiceAccountName: "service-account", - DisableCreation: ptr.To(false), - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Case #2", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DefaultNamespace: chev2.DefaultNamespace{ - AutoProvision: ptr.To(false), - }, - ServiceAccount: "service-account", - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - ServiceAccountName: "service-account", - DisableCreation: ptr.To(true), - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Case #3", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{}, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - DisableCreation: ptr.To(false), - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Case #4", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DefaultNamespace: chev2.DefaultNamespace{ - AutoProvision: ptr.To(false), - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - DisableCreation: ptr.To(false), - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.ServiceAccount, dwoc.Config.Workspace.ServiceAccount) - }) - } -} - -func TestReconcileDevWorkspaceConfigPodSchedulerName(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig with podSchedulerName", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PodSchedulerName: "test-scheduler", - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - SchedulerName: "test-scheduler", - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when PodSchedulerName is added", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PodSchedulerName: "test-scheduler", - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - SchedulerName: "test-scheduler", - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when PodSchedulerName is changed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PodSchedulerName: "test-scheduler", - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - SchedulerName: "previous-scheduler", - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - SchedulerName: "test-scheduler", - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when PodSchedulerName is removed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PodSchedulerName: "", - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - SchedulerName: "previous-scheduler", - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - DeploymentStrategy: "Recreate", - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.SchedulerName, dwoc.Config.Workspace.SchedulerName) - }) - } -} - -func TestReconcileDevWorkspaceConfigRuntimeClassName(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig with RuntimeClassName", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - RuntimeClassName: ptr.To("test-runtime-class"), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - RuntimeClassName: ptr.To("test-runtime-class"), - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when RuntimeClassName is added", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - RuntimeClassName: ptr.To("test-runtime-class"), - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - RuntimeClassName: ptr.To("test-runtime-class"), - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when RuntimeClassName is changed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - RuntimeClassName: ptr.To("test-runtime-class"), - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - RuntimeClassName: ptr.To("previous-runtime-class"), - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - RuntimeClassName: ptr.To("test-runtime-class"), - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when RuntimeClassName is removed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - RuntimeClassName: nil, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - RuntimeClassName: ptr.To("previous-runtime-class"), - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.SchedulerName, dwoc.Config.Workspace.SchedulerName) - }) - } -} - -func TestReconcileDevWorkspaceConfigServiceAccountTokens(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig with ServiceAccountTokens", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ - { - Name: "test-token-1", - MountPath: "/var/run/secrets/tokens", - Audience: "openshift", - ExpirationSeconds: 3600, - }, - { - Name: "test-token-2", - MountPath: "/var/run/secrets/tokens", - Audience: "kubernetes", - ExpirationSeconds: 180, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ - { - Name: "test-token-1", - MountPath: "/var/run/secrets/tokens", - Audience: "openshift", - ExpirationSeconds: 3600, - }, - { - Name: "test-token-2", - MountPath: "/var/run/secrets/tokens", - Audience: "kubernetes", - ExpirationSeconds: 180, - }, - }}, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when ServiceAccountTokens are added", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ - { - Name: "test-token", - MountPath: "/var/run/secrets/tokens", - Audience: "openshift", - ExpirationSeconds: 3600, - }, - { - Name: "test-token-2", - MountPath: "/var/run/secrets/tokens", - Audience: "kubernetes", - ExpirationSeconds: 180, - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ - { - Name: "test-token", - MountPath: "/var/run/secrets/tokens", - Audience: "openshift", - ExpirationSeconds: 3600, - }, - { - Name: "test-token-2", - MountPath: "/var/run/secrets/tokens", - Audience: "kubernetes", - ExpirationSeconds: 180, - }, - }, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when ServiceAccountTokens are changed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ - { - Name: "new-token", - MountPath: "/var/run/secrets/tokens", - Audience: "openshift", - ExpirationSeconds: 3600, - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ - { - Name: "old-token", - MountPath: "/var/run/secrets/tokens", - Audience: "openshift", - ExpirationSeconds: 60, - }, - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ - { - Name: "new-token", - MountPath: "/var/run/secrets/tokens", - Audience: "openshift", - ExpirationSeconds: 3600, - }, - }, - }, - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when ServiceAccountTokens are removed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{}, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{ - ServiceAccountTokens: []controllerv1alpha1.ServiceAccountToken{ - { - Name: "test-token", - MountPath: "/var/run/secrets/tokens", - Audience: "openshift", - ExpirationSeconds: 3600, - }, - { - Name: "test-token-2", - MountPath: "/var/run/secrets/tokens", - Audience: "kubernetes", - ExpirationSeconds: 180, - }, - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ServiceAccount: &controllerv1alpha1.ServiceAccountConfig{}, - DeploymentStrategy: "Recreate", - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.ServiceAccount.ServiceAccountTokens, dwoc.Config.Workspace.ServiceAccount.ServiceAccountTokens) - }) - } -} - -func TestReconcileDevWorkspaceConfigDeploymentStrategy(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig with DeploymentStrategy", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DeploymentStrategy: "Recreate", - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when DeploymentStrategy is added", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DeploymentStrategy: "Recreate", - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - DeploymentStrategy: "Recreate", - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig when DeploymentStrategy is changed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DeploymentStrategy: "RollingUpdate", - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - DeploymentStrategy: "Recreate", - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - DeploymentStrategy: "RollingUpdate", - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.DeploymentStrategy, dwoc.Config.Workspace.DeploymentStrategy) - }) - } -} - -func TestReconcileDevWorkspaceProjectCloneConfig(t *testing.T) { - const testNamespace = "eclipse-che" - testMemLimit := resource.MustParse("2Gi") - testCpuLimit := resource.MustParse("1000m") - testMemRequest := resource.MustParse("1Gi") - testCpuRequest := resource.MustParse("500m") - - type testCase struct { - name string - cheProjectCloneConfig *chev2.Container - expectedDevWorkspaceConfig *controllerv1alpha1.ProjectCloneConfig - existingDevWorkspaceConfig *controllerv1alpha1.ProjectCloneConfig - } - - tests := []testCase{ - { - name: "Syncs Che project clone config to DevWorkspaceOperatorConfig", - cheProjectCloneConfig: &chev2.Container{ - Name: "project-clone", - Image: "test-image", - ImagePullPolicy: "IfNotPresent", - Env: []corev1.EnvVar{ - {Name: "test-env-1", Value: "test-val-1"}, - {Name: "test-env-2", Value: "test-val-2"}, - }, - Resources: &chev2.ResourceRequirements{ - Limits: &chev2.ResourceList{ - Memory: &testMemLimit, - Cpu: &testCpuLimit, - }, - Requests: &chev2.ResourceList{ - Memory: &testMemRequest, - Cpu: &testCpuRequest, - }, - }, - }, - expectedDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ - Image: "test-image", - ImagePullPolicy: "IfNotPresent", - Env: []corev1.EnvVar{ - {Name: "test-env-1", Value: "test-val-1"}, - {Name: "test-env-2", Value: "test-val-2"}, - }, - Resources: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: testMemLimit, - corev1.ResourceCPU: testCpuLimit, - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: testMemRequest, - corev1.ResourceCPU: testCpuRequest, - }, - }, - }, - }, - { - name: "Updates existing DevWorkspaceOperatorConfig with new Che project clone config", - cheProjectCloneConfig: &chev2.Container{ - Name: "project-clone", - Image: "test-image", - ImagePullPolicy: "IfNotPresent", - Env: []corev1.EnvVar{ - {Name: "test-env-1", Value: "test-val-1"}, - {Name: "test-env-2", Value: "test-val-2"}, - }, - Resources: &chev2.ResourceRequirements{ - Limits: &chev2.ResourceList{ - Memory: &testMemLimit, - Cpu: &testCpuLimit, - }, - Requests: &chev2.ResourceList{ - Memory: &testMemRequest, - Cpu: &testCpuRequest, - }, - }, - }, - expectedDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ - Image: "test-image", - ImagePullPolicy: "IfNotPresent", - Env: []corev1.EnvVar{ - {Name: "test-env-1", Value: "test-val-1"}, - {Name: "test-env-2", Value: "test-val-2"}, - }, - Resources: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: testMemLimit, - corev1.ResourceCPU: testCpuLimit, - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: testMemRequest, - corev1.ResourceCPU: testCpuRequest, - }, - }, - }, - existingDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ - Image: "other image", - ImagePullPolicy: "Always", - Env: []corev1.EnvVar{ - {Name: "other-env", Value: "other-val"}, - }, - Resources: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1234Mi"), - corev1.ResourceCPU: resource.MustParse("1234m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1111Mi"), - corev1.ResourceCPU: resource.MustParse("1111m"), - }, - }, - }, - }, - { - name: "Removes fields from existing config when removed from CheCluster", - cheProjectCloneConfig: &chev2.Container{ - Name: "", - Image: "", - ImagePullPolicy: "", - Env: nil, - Resources: nil, - }, - expectedDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ - Image: "", - ImagePullPolicy: "", - Env: nil, - Resources: nil, - }, - existingDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ - Image: "other image", - ImagePullPolicy: "Always", - Env: []corev1.EnvVar{ - {Name: "other-env", Value: "other-val"}, - }, - Resources: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1234Mi"), - corev1.ResourceCPU: resource.MustParse("1234m"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1111Mi"), - corev1.ResourceCPU: resource.MustParse("1111m"), - }, - }, - }, - }, - } - - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - cheCluster := &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: testNamespace, - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - ProjectCloneContainer: testCase.cheProjectCloneConfig, - }, - }, - } - existingDWOC := &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: testNamespace, - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ProjectCloneConfig: testCase.existingDevWorkspaceConfig, - }, - }, - } - runtimeDWOC := client.Object(existingDWOC) - - deployContext := test.NewCtxBuilder().WithCheCluster(cheCluster).WithObjects(runtimeDWOC).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testNamespace}, dwoc) - assert.NoError(t, err) - - diff := cmp.Diff(testCase.expectedDevWorkspaceConfig, dwoc.Config.Workspace.ProjectCloneConfig) - assert.Empty(t, diff) - }) - } - -} - -func TestReconcileDevWorkspaceConfigPersistUserHome(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig when PersistUserHome is enabled", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PersistUserHome: &chev2.PersistentHomeConfig{ - Enabled: ptr.To(true), - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(true), - }, - }, - }, - }, - { - name: "Update existing DevWorkspaceOperatorConfig that does not have PersistUserHome config defined, when PersistUserHome is enabled", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PersistUserHome: &chev2.PersistentHomeConfig{ - Enabled: ptr.To(true), - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(true), - }, - }, - }, - }, - { - name: "Set DevWorkspaceOperatorConfig PersistUserHome enabled when PersistUserHome is enabled", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PersistUserHome: &chev2.PersistentHomeConfig{ - Enabled: ptr.To(true), - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(false), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(true), - }, - }, - }, - }, - { - name: "Set DevWorkspaceOperatorConfig PersistUserHome disableInitContainer to true", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PersistUserHome: &chev2.PersistentHomeConfig{ - Enabled: ptr.To(true), - DisableInitContainer: ptr.To(true), - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(false), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(true), - DisableInitContainer: ptr.To(true), - }, - }, - }, - }, - { - name: "Set DevWorkspaceOperatorConfig PersistUserHome disableInitContainer to false when initially true", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PersistUserHome: &chev2.PersistentHomeConfig{ - Enabled: ptr.To(true), - DisableInitContainer: ptr.To(false), - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(true), - DisableInitContainer: ptr.To(true), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(true), - DisableInitContainer: ptr.To(false), - }, - }, - }, - }, - { - name: "Set DevWorkspaceOperatorConfig PersistUserHome disabled when PersistUserHome is disabled", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - PersistUserHome: &chev2.PersistentHomeConfig{ - Enabled: ptr.To(false), - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(true), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(false), - }, - }, - }, - }, - { - name: "Remove PersistUserHome config from existing DevWorkspaceOperatorConfig when PersistUserHome is removed from Che Cluster", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{}, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ - Enabled: ptr.To(true), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - diff := cmp.Diff(testCase.expectedOperatorConfig, dwoc.Config, cmp.Options{cmpopts.IgnoreFields(controllerv1alpha1.WorkspaceConfig{}, "ServiceAccount", "DeploymentStrategy", "ContainerSecurityContext")}) - assert.Empty(t, diff) - }) - } -} - -func TestReconcileDevWorkspaceContainerSecurityContext(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig with Container security context", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - // We disable container build capabilities so that it does not override the container security context we configured - DisableContainerBuildCapabilities: ptr.To(true), - DisableContainerRunCapabilities: ptr.To(true), - Security: chev2.WorkspaceSecurityConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SYS_TIME", - "SETGID", - "SETUID", - }, - Drop: []corev1.Capability{ - "CHOWN", - "KILL", - }, - }, - AllowPrivilegeEscalation: ptr.To(false), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SYS_TIME", - "SETGID", - "SETUID", - }, - Drop: []corev1.Capability{ - "CHOWN", - "KILL", - }, - }, - AllowPrivilegeEscalation: ptr.To(false), - }, - }, - }, - }, - { - name: "Updates existing DevWorkspaceOperatorConfig when Container security context is added", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - DisableContainerRunCapabilities: ptr.To(true), - Security: chev2.WorkspaceSecurityConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SYS_TIME", - "SETGID", - "SETUID", - }, - Drop: []corev1.Capability{ - "CHOWN", - "KILL", - }, - }, - AllowPrivilegeEscalation: ptr.To(false), - }}, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SYS_TIME", - "SETGID", - "SETUID", - }, - Drop: []corev1.Capability{ - "CHOWN", - "KILL", - }, - }, - AllowPrivilegeEscalation: ptr.To(false), - }, - }, - }, - }, - { - name: "Updates existing DevWorkspaceOperatorConfig when Container security is changed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - DisableContainerRunCapabilities: ptr.To(true), - Security: chev2.WorkspaceSecurityConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SYS_TIME", - "SETGID", - "SETUID", - }, - Drop: []corev1.Capability{ - "CHOWN", - "KILL", - }, - }, - AllowPrivilegeEscalation: ptr.To(false), - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "KILL", - }, - Drop: []corev1.Capability{ - "SYS_TIME", - }, - }, - AllowPrivilegeEscalation: ptr.To(true), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "SYS_TIME", - "SETGID", - "SETUID", - }, - Drop: []corev1.Capability{ - "CHOWN", - "KILL", - }, - }, - AllowPrivilegeEscalation: ptr.To(false), - }, - }, - }, - }, - { - name: "Updates existing DevWorkspaceOperatorConfig when Container security is removed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DisableContainerBuildCapabilities: ptr.To(true), - DisableContainerRunCapabilities: ptr.To(true), - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ContainerSecurityContext: &corev1.SecurityContext{ - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{ - "KILL", - }, - Drop: []corev1.Capability{ - "SYS_TIME", - }, - }, - AllowPrivilegeEscalation: ptr.To(true), - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - - sortCapabilities := func(capabilities []corev1.Capability) func(i, j int) bool { - return func(i, j int) bool { - return capabilities[i] > capabilities[j] - } - } - expectedContainerSecurityContext := testCase.expectedOperatorConfig.Workspace.ContainerSecurityContext - actualContainerSecurityContext := dwoc.Config.Workspace.ContainerSecurityContext - if expectedContainerSecurityContext != nil { - sort.Slice(expectedContainerSecurityContext.Capabilities.Add, sortCapabilities(expectedContainerSecurityContext.Capabilities.Add)) - sort.Slice(expectedContainerSecurityContext.Capabilities.Drop, sortCapabilities(expectedContainerSecurityContext.Capabilities.Drop)) - } - if actualContainerSecurityContext != nil { - sort.Slice(actualContainerSecurityContext.Capabilities.Add, sortCapabilities(actualContainerSecurityContext.Capabilities.Add)) - sort.Slice(actualContainerSecurityContext.Capabilities.Drop, sortCapabilities(actualContainerSecurityContext.Capabilities.Drop)) - - } - - assert.Equal(t, expectedContainerSecurityContext, actualContainerSecurityContext, - fmt.Sprintf("Did not get expected ContainerSecurityContext.\nDiff:%s", cmp.Diff(expectedContainerSecurityContext, actualContainerSecurityContext))) - }) - } -} - -func TestReconcileDevWorkspacePodSecurityContext(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - configuredPodSecurityContext := &corev1.PodSecurityContext{ - RunAsUser: ptr.To(int64(0)), - RunAsGroup: ptr.To(int64(0)), - RunAsNonRoot: ptr.To(false), - } - - var testCases = []testCase{ - { - name: "Create DevWorkspaceOperatorConfig with Pod security context", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - Security: chev2.WorkspaceSecurityConfig{ - PodSecurityContext: configuredPodSecurityContext, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PodSecurityContext: configuredPodSecurityContext, - }, - }, - }, - { - name: "Updates existing DevWorkspaceOperatorConfig with Pod security context", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - Security: chev2.WorkspaceSecurityConfig{ - PodSecurityContext: configuredPodSecurityContext, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PodSecurityContext: configuredPodSecurityContext, - }, - }, - }, - { - name: "Updates existing DevWorkspaceOperatorConfig when Pod security context is changed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - Security: chev2.WorkspaceSecurityConfig{ - PodSecurityContext: configuredPodSecurityContext, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PodSecurityContext: &corev1.PodSecurityContext{ - RunAsUser: ptr.To(int64(1000)), - RunAsGroup: ptr.To(int64(10001)), - RunAsNonRoot: ptr.To(true), - SupplementalGroups: []int64{ - 5, - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PodSecurityContext: configuredPodSecurityContext, - }, - }, - }, - { - name: "Updates existing DevWorkspaceOperatorConfig when Pod security context is removed", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{}, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PodSecurityContext: &corev1.PodSecurityContext{ - RunAsUser: ptr.To(int64(1000)), - RunAsGroup: ptr.To(int64(10001)), - RunAsNonRoot: ptr.To(true), - SupplementalGroups: []int64{ - 5, - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.PodSecurityContext, dwoc.Config.Workspace.PodSecurityContext, - fmt.Sprintf("Did not get expected PodSecurityContext.\nDiff:%s", cmp.Diff(testCase.expectedOperatorConfig.Workspace.PodSecurityContext, dwoc.Config.Workspace.PodSecurityContext))) - }) - } -} - -func TestReconcileDevWorkspaceImagePullPolicy(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Set specific pull policy", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - ImagePullPolicy: corev1.PullAlways, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ImagePullPolicy: string(corev1.PullAlways), - }, - }, - }, - { - name: "Clean up pull policy", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - ImagePullPolicy: "", - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ImagePullPolicy: "", - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - ImagePullPolicy: string(corev1.PullAlways), - }, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.ImagePullPolicy, dwoc.Config.Workspace.ImagePullPolicy) - }) - } -} - -func TestReconcileDevWorkspaceAnnotations(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Set specific annotations", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - WorkspacesPodAnnotations: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PodAnnotations: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - }, - }, - }, - { - name: "Remove annotations", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - WorkspacesPodAnnotations: nil, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PodAnnotations: nil, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - PodAnnotations: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - }, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.PodAnnotations, dwoc.Config.Workspace.PodAnnotations) - }) - } -} - -func TestReconcileDevWorkspaceIgnoredUnrecoverableEvents(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - var testCases = []testCase{ - { - name: "Set events", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - IgnoredUnrecoverableEvents: []string{ - "value1", - "value2", - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - IgnoredUnrecoverableEvents: []string{ - "value1", - "value2", - }, - }, - }, - }, - { - name: "Remove events", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - IgnoredUnrecoverableEvents: nil, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - IgnoredUnrecoverableEvents: nil, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - IgnoredUnrecoverableEvents: []string{ - "value1", - "value2", - }, - }, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.IgnoredUnrecoverableEvents, dwoc.Config.Workspace.IgnoredUnrecoverableEvents) - }) - } -} - -func TestReconcileDevWorkspaceConfigForInitContainers(t *testing.T) { - type testCase struct { - name string - cheCluster *chev2.CheCluster - existedObjects []client.Object - expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration - } - - testCases := []testCase{ - { - name: "Create DevWorkspaceOperatorConfig with InitContainers", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - InitContainers: []corev1.Container{ - { - Name: "init-container-1", - Image: "init-image:latest", - Command: []string{ - "/bin/sh", - "-c", - "echo 'Initializing workspace'", - }, - }, - { - Name: "init-container-2", - Image: "init-image-2:v1.0", - Env: []corev1.EnvVar{ - { - Name: "INIT_VAR", - Value: "init-value", - }, - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - InitContainers: []corev1.Container{ - { - Name: "init-container-1", - Image: "init-image:latest", - Command: []string{ - "/bin/sh", - "-c", - "echo 'Initializing workspace'", - }, - }, - { - Name: "init-container-2", - Image: "init-image-2:v1.0", - Env: []corev1.EnvVar{ - { - Name: "INIT_VAR", - Value: "init-value", - }, - }, - }, - }, - }, - }, - }, - { - name: "Create DevWorkspaceOperatorConfig without InitContainers", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{}, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - { - name: "Update DevWorkspaceOperatorConfig with InitContainers", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - InitContainers: []corev1.Container{ - { - Name: "new-init-container", - Image: "new-init:v2.0", - VolumeMounts: []corev1.VolumeMount{ - { - Name: "config-volume", - MountPath: "/etc/config", - }, - }, - }, - }, - }, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - InitContainers: []corev1.Container{ - { - Name: "old-init-container", - Image: "old-init:v1.0", - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - InitContainers: []corev1.Container{ - { - Name: "new-init-container", - Image: "new-init:v2.0", - VolumeMounts: []corev1.VolumeMount{ - { - Name: "config-volume", - MountPath: "/etc/config", - }, - }, - }, - }, - }, - }, - }, - { - name: "Clear InitContainers from DevWorkspaceOperatorConfig", - cheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{}, - }, - }, - existedObjects: []client.Object{ - &controllerv1alpha1.DevWorkspaceOperatorConfig{ - ObjectMeta: metav1.ObjectMeta{ - Name: devWorkspaceConfigName, - Namespace: "eclipse-che", - }, - TypeMeta: metav1.TypeMeta{ - Kind: "DevWorkspaceOperatorConfig", - APIVersion: controllerv1alpha1.GroupVersion.String(), - }, - Config: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{ - InitContainers: []corev1.Container{ - { - Name: "init-to-remove", - Image: "init:v1.0", - }, - }, - }, - }, - }, - }, - expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ - Workspace: &controllerv1alpha1.WorkspaceConfig{}, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() - - devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() - test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) - - dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) - assert.NoError(t, err) - - assert.Equal(t, testCase.expectedOperatorConfig.Workspace.InitContainers, dwoc.Config.Workspace.InitContainers) - }) - } -} diff --git a/pkg/deploy/devworkspace/dev_workspace_config_workspace_test.go b/pkg/deploy/devworkspace/dev_workspace_config_workspace_test.go new file mode 100644 index 000000000..43cc2ac24 --- /dev/null +++ b/pkg/deploy/devworkspace/dev_workspace_config_workspace_test.go @@ -0,0 +1,978 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "context" + "testing" + + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestReconcileDevWorkspaceProjectCloneConfig(t *testing.T) { + const testNamespace = "eclipse-che" + testMemLimit := resource.MustParse("2Gi") + testCpuLimit := resource.MustParse("1000m") + testMemRequest := resource.MustParse("1Gi") + testCpuRequest := resource.MustParse("500m") + + type testCase struct { + name string + cheProjectCloneConfig *chev2.Container + expectedDevWorkspaceConfig *controllerv1alpha1.ProjectCloneConfig + existingDevWorkspaceConfig *controllerv1alpha1.ProjectCloneConfig + } + + tests := []testCase{ + { + name: "Syncs Che project clone config to DevWorkspaceOperatorConfig", + cheProjectCloneConfig: &chev2.Container{ + Name: "project-clone", + Image: "test-image", + ImagePullPolicy: "IfNotPresent", + Env: []corev1.EnvVar{ + {Name: "test-env-1", Value: "test-val-1"}, + {Name: "test-env-2", Value: "test-val-2"}, + }, + Resources: &chev2.ResourceRequirements{ + Limits: &chev2.ResourceList{ + Memory: &testMemLimit, + Cpu: &testCpuLimit, + }, + Requests: &chev2.ResourceList{ + Memory: &testMemRequest, + Cpu: &testCpuRequest, + }, + }, + }, + expectedDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ + Image: "test-image", + ImagePullPolicy: "IfNotPresent", + Env: []corev1.EnvVar{ + {Name: "test-env-1", Value: "test-val-1"}, + {Name: "test-env-2", Value: "test-val-2"}, + }, + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: testMemLimit, + corev1.ResourceCPU: testCpuLimit, + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: testMemRequest, + corev1.ResourceCPU: testCpuRequest, + }, + }, + }, + }, + { + name: "Updates existing DevWorkspaceOperatorConfig with new Che project clone config", + cheProjectCloneConfig: &chev2.Container{ + Name: "project-clone", + Image: "test-image", + ImagePullPolicy: "IfNotPresent", + Env: []corev1.EnvVar{ + {Name: "test-env-1", Value: "test-val-1"}, + {Name: "test-env-2", Value: "test-val-2"}, + }, + Resources: &chev2.ResourceRequirements{ + Limits: &chev2.ResourceList{ + Memory: &testMemLimit, + Cpu: &testCpuLimit, + }, + Requests: &chev2.ResourceList{ + Memory: &testMemRequest, + Cpu: &testCpuRequest, + }, + }, + }, + expectedDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ + Image: "test-image", + ImagePullPolicy: "IfNotPresent", + Env: []corev1.EnvVar{ + {Name: "test-env-1", Value: "test-val-1"}, + {Name: "test-env-2", Value: "test-val-2"}, + }, + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: testMemLimit, + corev1.ResourceCPU: testCpuLimit, + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: testMemRequest, + corev1.ResourceCPU: testCpuRequest, + }, + }, + }, + existingDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ + Image: "other image", + ImagePullPolicy: "Always", + Env: []corev1.EnvVar{ + {Name: "other-env", Value: "other-val"}, + }, + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1234Mi"), + corev1.ResourceCPU: resource.MustParse("1234m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1111Mi"), + corev1.ResourceCPU: resource.MustParse("1111m"), + }, + }, + }, + }, + { + name: "Removes fields from existing config when removed from CheCluster", + cheProjectCloneConfig: &chev2.Container{ + Name: "", + Image: "", + ImagePullPolicy: "", + Env: nil, + Resources: nil, + }, + expectedDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ + Image: "", + ImagePullPolicy: "", + Env: nil, + Resources: nil, + }, + existingDevWorkspaceConfig: &controllerv1alpha1.ProjectCloneConfig{ + Image: "other image", + ImagePullPolicy: "Always", + Env: []corev1.EnvVar{ + {Name: "other-env", Value: "other-val"}, + }, + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1234Mi"), + corev1.ResourceCPU: resource.MustParse("1234m"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1111Mi"), + corev1.ResourceCPU: resource.MustParse("1111m"), + }, + }, + }, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + cheCluster := &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + ProjectCloneContainer: testCase.cheProjectCloneConfig, + }, + }, + } + existingDWOC := &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: testNamespace, + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ProjectCloneConfig: testCase.existingDevWorkspaceConfig, + }, + }, + } + runtimeDWOC := client.Object(existingDWOC) + + deployContext := test.NewCtxBuilder().WithCheCluster(cheCluster).WithObjects(runtimeDWOC).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testNamespace}, dwoc) + assert.NoError(t, err) + + diff := cmp.Diff(testCase.expectedDevWorkspaceConfig, dwoc.Config.Workspace.ProjectCloneConfig) + assert.Empty(t, diff) + }) + } + +} + +func TestReconcileDevWorkspaceConfigPersistUserHome(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Create DevWorkspaceOperatorConfig when PersistUserHome is enabled", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PersistUserHome: &chev2.PersistentHomeConfig{ + Enabled: ptr.To(true), + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(true), + }, + }, + }, + }, + { + name: "Update existing DevWorkspaceOperatorConfig that does not have PersistUserHome config defined, when PersistUserHome is enabled", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PersistUserHome: &chev2.PersistentHomeConfig{ + Enabled: ptr.To(true), + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(true), + }, + }, + }, + }, + { + name: "Set DevWorkspaceOperatorConfig PersistUserHome enabled when PersistUserHome is enabled", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PersistUserHome: &chev2.PersistentHomeConfig{ + Enabled: ptr.To(true), + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(false), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(true), + }, + }, + }, + }, + { + name: "Set DevWorkspaceOperatorConfig PersistUserHome disableInitContainer to true", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PersistUserHome: &chev2.PersistentHomeConfig{ + Enabled: ptr.To(true), + DisableInitContainer: ptr.To(true), + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(false), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(true), + DisableInitContainer: ptr.To(true), + }, + }, + }, + }, + { + name: "Set DevWorkspaceOperatorConfig PersistUserHome disableInitContainer to false when initially true", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PersistUserHome: &chev2.PersistentHomeConfig{ + Enabled: ptr.To(true), + DisableInitContainer: ptr.To(false), + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(true), + DisableInitContainer: ptr.To(true), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(true), + DisableInitContainer: ptr.To(false), + }, + }, + }, + }, + { + name: "Set DevWorkspaceOperatorConfig PersistUserHome disabled when PersistUserHome is disabled", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + PersistUserHome: &chev2.PersistentHomeConfig{ + Enabled: ptr.To(false), + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(true), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(false), + }, + }, + }, + }, + { + name: "Remove PersistUserHome config from existing DevWorkspaceOperatorConfig when PersistUserHome is removed from Che Cluster", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{}, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PersistUserHome: &controllerv1alpha1.PersistentHomeConfig{ + Enabled: ptr.To(true), + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + diff := cmp.Diff(testCase.expectedOperatorConfig, dwoc.Config, cmp.Options{ + cmpopts.IgnoreFields(controllerv1alpha1.WorkspaceConfig{}, "ServiceAccount", "DeploymentStrategy", "ContainerSecurityContext"), + cmpopts.IgnoreFields(controllerv1alpha1.RoutingConfig{}, "TLSCertificateConfigmapRef"), + }) + assert.Empty(t, diff) + }) + } +} + +func TestReconcileDevWorkspaceImagePullPolicy(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Set specific pull policy", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + ImagePullPolicy: corev1.PullAlways, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ImagePullPolicy: string(corev1.PullAlways), + }, + }, + }, + { + name: "Clean up pull policy", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + ImagePullPolicy: "", + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ImagePullPolicy: "", + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + ImagePullPolicy: string(corev1.PullAlways), + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.ImagePullPolicy, dwoc.Config.Workspace.ImagePullPolicy) + }) + } +} + +func TestReconcileDevWorkspaceAnnotations(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Set specific annotations", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + WorkspacesPodAnnotations: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodAnnotations: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + }, + { + name: "Remove annotations", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + WorkspacesPodAnnotations: nil, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodAnnotations: nil, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + PodAnnotations: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.PodAnnotations, dwoc.Config.Workspace.PodAnnotations) + }) + } +} + +func TestReconcileDevWorkspaceIgnoredUnrecoverableEvents(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + var testCases = []testCase{ + { + name: "Set events", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + IgnoredUnrecoverableEvents: []string{ + "value1", + "value2", + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + IgnoredUnrecoverableEvents: []string{ + "value1", + "value2", + }, + }, + }, + }, + { + name: "Remove events", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + IgnoredUnrecoverableEvents: nil, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + IgnoredUnrecoverableEvents: nil, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + IgnoredUnrecoverableEvents: []string{ + "value1", + "value2", + }, + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.IgnoredUnrecoverableEvents, dwoc.Config.Workspace.IgnoredUnrecoverableEvents) + }) + } +} + +func TestReconcileDevWorkspaceConfigForInitContainers(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + existedObjects []client.Object + expectedOperatorConfig *controllerv1alpha1.OperatorConfiguration + } + + testCases := []testCase{ + { + name: "Create DevWorkspaceOperatorConfig with InitContainers", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + InitContainers: []corev1.Container{ + { + Name: "init-container-1", + Image: "init-image:latest", + Command: []string{ + "/bin/sh", + "-c", + "echo 'Initializing workspace'", + }, + }, + { + Name: "init-container-2", + Image: "init-image-2:v1.0", + Env: []corev1.EnvVar{ + { + Name: "INIT_VAR", + Value: "init-value", + }, + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + InitContainers: []corev1.Container{ + { + Name: "init-container-1", + Image: "init-image:latest", + Command: []string{ + "/bin/sh", + "-c", + "echo 'Initializing workspace'", + }, + }, + { + Name: "init-container-2", + Image: "init-image-2:v1.0", + Env: []corev1.EnvVar{ + { + Name: "INIT_VAR", + Value: "init-value", + }, + }, + }, + }, + }, + }, + }, + { + name: "Create DevWorkspaceOperatorConfig without InitContainers", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{}, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + { + name: "Update DevWorkspaceOperatorConfig with InitContainers", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{ + InitContainers: []corev1.Container{ + { + Name: "new-init-container", + Image: "new-init:v2.0", + VolumeMounts: []corev1.VolumeMount{ + { + Name: "config-volume", + MountPath: "/etc/config", + }, + }, + }, + }, + }, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + InitContainers: []corev1.Container{ + { + Name: "old-init-container", + Image: "old-init:v1.0", + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + InitContainers: []corev1.Container{ + { + Name: "new-init-container", + Image: "new-init:v2.0", + VolumeMounts: []corev1.VolumeMount{ + { + Name: "config-volume", + MountPath: "/etc/config", + }, + }, + }, + }, + }, + }, + }, + { + name: "Clear InitContainers from DevWorkspaceOperatorConfig", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + DevEnvironments: chev2.CheClusterDevEnvironments{}, + }, + }, + existedObjects: []client.Object{ + &controllerv1alpha1.DevWorkspaceOperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: devWorkspaceConfigName, + Namespace: "eclipse-che", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "DevWorkspaceOperatorConfig", + APIVersion: controllerv1alpha1.GroupVersion.String(), + }, + Config: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{ + InitContainers: []corev1.Container{ + { + Name: "init-to-remove", + Image: "init:v1.0", + }, + }, + }, + }, + }, + }, + expectedOperatorConfig: &controllerv1alpha1.OperatorConfiguration{ + Workspace: &controllerv1alpha1.WorkspaceConfig{}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + deployContext := test.NewCtxBuilder().WithCheCluster(testCase.cheCluster).WithObjects(testCase.existedObjects...).Build() + + devWorkspaceConfigReconciler := NewDevWorkspaceConfigReconciler() + test.EnsureReconcile(t, deployContext, devWorkspaceConfigReconciler.Reconcile) + + dwoc := &controllerv1alpha1.DevWorkspaceOperatorConfig{} + err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: devWorkspaceConfigName, Namespace: testCase.cheCluster.Namespace}, dwoc) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedOperatorConfig.Workspace.InitContainers, dwoc.Config.Workspace.InitContainers) + }) + } +} diff --git a/pkg/deploy/labels.go b/pkg/deploy/labels.go index 9defe566a..cdfdb85d3 100644 --- a/pkg/deploy/labels.go +++ b/pkg/deploy/labels.go @@ -13,18 +13,12 @@ package deploy import ( + "maps" + "slices" + "github.com/eclipse-che/che-operator/pkg/common/constants" defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" -) - -var ( - DefaultsLabelKeys = []string{ - constants.KubernetesNameLabelKey, - constants.KubernetesInstanceLabelKey, - constants.KubernetesPartOfLabelKey, - constants.KubernetesComponentLabelKey, - constants.KubernetesManagedByLabelKey, - } + "sigs.k8s.io/controller-runtime/pkg/client" ) func GetLabels(component string) map[string]string { @@ -37,6 +31,16 @@ func GetLabels(component string) map[string]string { } } +func GetLabelKeys() []string { + return []string{ + constants.KubernetesNameLabelKey, + constants.KubernetesInstanceLabelKey, + constants.KubernetesPartOfLabelKey, + constants.KubernetesComponentLabelKey, + constants.KubernetesManagedByLabelKey, + } +} + func GetManagedByLabel() string { return defaults.GetCheFlavor() + "-operator" } @@ -61,6 +65,12 @@ func GetLegacyLabels(component string) map[string]string { } } +// GetLabelsAndAnnotations extracts label and annotation keys from an object. +// Note: key order is non-deterministic (map iteration order). +func GetLabelsAndAnnotations(obj client.Object) ([]string, []string) { + return slices.Collect(maps.Keys(obj.GetLabels())), slices.Collect(maps.Keys(obj.GetAnnotations())) +} + func IsPartOfEclipseCheResourceAndManagedByOperator(labels map[string]string) bool { return labels[constants.KubernetesPartOfLabelKey] == constants.CheEclipseOrg && labels[constants.KubernetesManagedByLabelKey] == GetManagedByLabel() } diff --git a/pkg/deploy/server/chehost_reconciler_test.go b/pkg/deploy/server/chehost_reconciler_test.go index e01e9d3bd..6ae32b913 100644 --- a/pkg/deploy/server/chehost_reconciler_test.go +++ b/pkg/deploy/server/chehost_reconciler_test.go @@ -55,8 +55,7 @@ func TestSyncService(t *testing.T) { assert.Nil(t, err) service := &corev1.Service{} - done, err = deploy.Get(ctx, types.NamespacedName{Name: deploy.CheServiceName, Namespace: "eclipse-che"}, service) - assert.True(t, done) + err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: deploy.CheServiceName, Namespace: "eclipse-che"}, service) assert.Nil(t, err) assert.Equal(t, service.Spec.Ports[0].Name, "http") diff --git a/pkg/deploy/sync.go b/pkg/deploy/sync.go index 37cebdd71..11ba26e2a 100644 --- a/pkg/deploy/sync.go +++ b/pkg/deploy/sync.go @@ -68,21 +68,6 @@ func SyncForClient(cli client.Client, deployContext *chetypes.DeployContext, blu return doUpdate(cli, deployContext, actual.(client.Object), blueprint, diffOpts...) } -// Get gets object. -// Returns true if object exists otherwise returns false. -// Returns error if object cannot be retrieved otherwise returns nil. -func Get(deployContext *chetypes.DeployContext, key client.ObjectKey, actual client.Object) (bool, error) { - cli := getClientForObject(key.Namespace, deployContext) - return doGet(context.TODO(), cli, key, actual) -} - -// Get gets object. -// Returns true if object exists otherwise returns false. -// Returns error if object cannot be retrieved otherwise returns nil. -func GetForClient(cli client.Client, key client.ObjectKey, actual client.Object) (bool, error) { - return doGet(context.TODO(), cli, key, actual) -} - // Gets namespaced scope object by name // Returns true if object exists otherwise returns false. func GetNamespacedObject(deployContext *chetypes.DeployContext, name string, actual client.Object) (bool, error) { @@ -120,12 +105,6 @@ func DeleteNamespacedObject(deployContext *chetypes.DeployContext, name string, return DeleteByKeyWithClient(client, key, objectMeta) } -func DeleteClusterObject(deployContext *chetypes.DeployContext, name string, objectMeta client.Object) (bool, error) { - client := deployContext.ClusterAPI.NonCachingClient - key := types.NamespacedName{Name: name} - return DeleteByKeyWithClient(client, key, objectMeta) -} - func DeleteByKeyWithClient(cli client.Client, key client.ObjectKey, objectMeta client.Object) (bool, error) { runtimeObject, ok := objectMeta.(runtime.Object) if !ok { diff --git a/pkg/deploy/sync_test.go b/pkg/deploy/sync_test.go index c6feb3d27..a585ab4fc 100644 --- a/pkg/deploy/sync_test.go +++ b/pkg/deploy/sync_test.go @@ -54,21 +54,6 @@ var ( testKey = client.ObjectKey{Name: "test-secret", Namespace: "eclipse-che"} ) -func TestGet(t *testing.T) { - ctx := test.NewCtxBuilder().Build() - - err := ctx.ClusterAPI.Client.Create(context.TODO(), testObj.DeepCopy()) - if err != nil { - t.Fatalf("Failed to create object: %v", err) - } - - actual := &corev1.Secret{} - exists, err := Get(ctx, testKey, actual) - if !exists || err != nil { - t.Fatalf("Failed to get object: %v", err) - } -} - func TestCreateIgnoreIfExistsShouldReturnTrueIfObjectCreated(t *testing.T) { ctx := test.NewCtxBuilder().Build() diff --git a/pkg/deploy/tls/certificates.go b/pkg/deploy/tls/certificates.go index 291d419d6..518d61452 100644 --- a/pkg/deploy/tls/certificates.go +++ b/pkg/deploy/tls/certificates.go @@ -100,7 +100,7 @@ func (c *CertificatesReconciler) Reconcile(ctx *chetypes.DeployContext) (reconci return reconcile.Result{}, true, nil } -func (c *CertificatesReconciler) Finalize(ctx *chetypes.DeployContext) bool { +func (c *CertificatesReconciler) Finalize(_ctx *chetypes.DeployContext) bool { return true } @@ -114,9 +114,9 @@ func (c *CertificatesReconciler) syncOpenShiftCABundleCertificates(ctx *chetypes // It might contain custom certificates added there before the doc has been introduced // https://eclipse.dev/che/docs/stable/administration-guide/importing-untrusted-tls-certificates/ openShiftCaBundleCM := &corev1.ConfigMap{} - exists, err := deploy.Get(ctx, openShiftCaBundleCMKey, openShiftCaBundleCM) + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound(context.TODO(), openShiftCaBundleCMKey, openShiftCaBundleCM) if err != nil { - return false, err + return false, fmt.Errorf("failed to read ConfigMap %s: %w", constants.DefaultCaBundleCertsCMName, err) } if !exists { @@ -128,12 +128,6 @@ func (c *CertificatesReconciler) syncOpenShiftCABundleCertificates(ctx *chetypes } } - // Ensure TypeMeta to avoid "cause: no version "" has been registered in scheme" error - openShiftCaBundleCM.TypeMeta = metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - } - openShiftCaBundleCM.Labels = utils.GetMapOrDefault(openShiftCaBundleCM.Labels, map[string]string{}) utils.AddMap(openShiftCaBundleCM.Labels, deploy.GetLabels(constants.CheCABundle)) @@ -156,32 +150,35 @@ func (c *CertificatesReconciler) syncOpenShiftCABundleCertificates(ctx *chetypes } trustedCACM := &corev1.ConfigMap{} - if exists, err := deploy.Get(ctx, trustedCACMKey, trustedCACM); exists { + if exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound(context.TODO(), trustedCACMKey, trustedCACM); exists { openShiftCaBundleCM.Data = utils.GetMapOrDefault(openShiftCaBundleCM.Data, map[string]string{}) openShiftCaBundleCM.Data["ca-bundle.crt"] = trustedCACM.Data["ca-bundle.crt"] } else if err != nil { return false, err } } - - return deploy.Sync( - ctx, - openShiftCaBundleCM, - diffs.ConfigMap(append(deploy.DefaultsLabelKeys, constants.ConfigOpenShiftIOInjectTrustedCaBundle), nil), - ) } else { // Add annotation to allow OpenShift network operator inject certificates // https://docs.redhat.com/en/documentation/openshift_container_platform/4.18/html/networking/configuring-a-custom-pki#certificate-injection-using-operators_configuring-a-custom-pki openShiftCaBundleCM.Labels[constants.ConfigOpenShiftIOInjectTrustedCaBundle] = "true" + } - // Ignore Data field to allow OpenShift network operator inject certificates into CM - // and avoid endless reconciliation loop - return deploy.Sync( - ctx, - openShiftCaBundleCM, - diffs.ConfigMap(append(deploy.DefaultsLabelKeys, constants.ConfigOpenShiftIOInjectTrustedCaBundle), nil), - ) + if err := controllerutil.SetControllerReference(ctx.CheCluster, openShiftCaBundleCM, ctx.ClusterAPI.Scheme); err != nil { + return false, err } + + // adding `ConfigOpenShiftIOInjectTrustedCaBundle` label (even if deleted) ensures + // that destination ConfigMap doesn't have it + mandatoryLabelKeys := slices.Concat(deploy.GetLabelKeys(), []string{constants.ConfigOpenShiftIOInjectTrustedCaBundle}) + + err = ctx.ClusterAPI.ClientWrapper.Sync( + context.TODO(), + openShiftCaBundleCM, + &k8sclient.SyncOptions{ + DiffOpts: diffs.ConfigMap(mandatoryLabelKeys, nil), + }) + + return err == nil, err } func (c *CertificatesReconciler) syncKubernetesCABundleCertificates(ctx *chetypes.DeployContext) (bool, error) { @@ -204,7 +201,19 @@ func (c *CertificatesReconciler) syncKubernetesCABundleCertificates(ctx *chetype Data: map[string]string{kubernetesCABundleCertsFile: string(data)}, } - return deploy.Sync(ctx, kubernetesCaBundleCM, diffs.ConfigMapEnsureLabels) + if err := controllerutil.SetControllerReference(ctx.CheCluster, kubernetesCaBundleCM, ctx.ClusterAPI.Scheme); err != nil { + return false, err + } + + err = ctx.ClusterAPI.ClientWrapper.Sync( + context.TODO(), + kubernetesCaBundleCM, + &k8sclient.SyncOptions{ + DiffOpts: diffs.ConfigMap(deploy.GetLabelKeys(), nil), + }, + ) + + return err == nil, err } // syncGitTrustedCertificates adds labels to git trusted certificates ConfigMap @@ -220,17 +229,12 @@ func (c *CertificatesReconciler) syncGitTrustedCertificates(ctx *chetypes.Deploy Name: ctx.CheCluster.Spec.DevEnvironments.TrustedCerts.GitTrustedCertsConfigMapName, } - exists, err := deploy.Get(ctx, gitTrustedCertsKey, gitTrustedCertsCM) + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound(context.TODO(), gitTrustedCertsKey, gitTrustedCertsCM) if !exists { return err == nil, err } if gitTrustedCertsCM.Data[constants.GitSelfSignedCertsConfigMapCertKey] != "" { - gitTrustedCertsCM.TypeMeta = metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - } - if gitTrustedCertsCM.GetLabels() == nil { gitTrustedCertsCM.Labels = map[string]string{} } @@ -239,11 +243,22 @@ func (c *CertificatesReconciler) syncGitTrustedCertificates(ctx *chetypes.Deploy gitTrustedCertsCM.Labels[constants.KubernetesPartOfLabelKey] = constants.CheEclipseOrg gitTrustedCertsCM.Labels[constants.KubernetesComponentLabelKey] = constants.CheCABundle - return deploy.Sync( - ctx, + // Don't need set SetControllerReference on this ConfigMap since it is created by admin + + err = ctx.ClusterAPI.ClientWrapper.Sync( + context.TODO(), gitTrustedCertsCM, - diffs.ConfigMap([]string{constants.KubernetesPartOfLabelKey, constants.KubernetesComponentLabelKey}, nil), + &k8sclient.SyncOptions{ + DiffOpts: diffs.ConfigMap( + []string{ + constants.KubernetesPartOfLabelKey, + constants.KubernetesComponentLabelKey}, + nil, + ), + }, ) + + return err == nil, err } return true, nil @@ -258,7 +273,7 @@ func (c *CertificatesReconciler) syncSelfSignedCertificates(ctx *chetypes.Deploy Namespace: ctx.CheCluster.Namespace, } - exists, err := deploy.Get(ctx, selfSignedCertSecretKey, selfSignedCertSecret) + exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound(context.TODO(), selfSignedCertSecretKey, selfSignedCertSecret) if !exists { return err == nil, err } @@ -278,23 +293,31 @@ func (c *CertificatesReconciler) syncSelfSignedCertificates(ctx *chetypes.Deploy Data: map[string]string{"ca.crt": string(selfSignedCertSecret.Data["ca.crt"])}, } - return deploy.Sync(ctx, selfSignedCertCM, diffs.ConfigMapEnsureLabels) + if err := controllerutil.SetControllerReference(ctx.CheCluster, selfSignedCertCM, ctx.ClusterAPI.Scheme); err != nil { + return false, err + } + + err = ctx.ClusterAPI.ClientWrapper.Sync( + context.TODO(), + selfSignedCertCM, + &k8sclient.SyncOptions{ + DiffOpts: diffs.ConfigMap(deploy.GetLabelKeys(), nil), + }) } - return true, nil + return err == nil, err } // syncKubernetesRootCertificates adds labels to `kube-root-ca.crt` ConfigMap // to include them into the final bundle func (c *CertificatesReconciler) syncKubernetesRootCertificates(ctx *chetypes.DeployContext) (bool, error) { - client := ctx.ClusterAPI.NonCachingClient kubeRootCertsCM := &corev1.ConfigMap{} kubeRootCertsCMKey := types.NamespacedName{ Name: kubernetesRootCACertsCMName, Namespace: ctx.CheCluster.Namespace, } - exists, err := deploy.GetForClient(client, kubeRootCertsCMKey, kubeRootCertsCM) + exists, err := ctx.ClusterAPI.NonCachingClientWrapper.GetIgnoreNotFound(context.TODO(), kubeRootCertsCMKey, kubeRootCertsCM) if !exists { return err == nil, err } @@ -303,22 +326,23 @@ func (c *CertificatesReconciler) syncKubernetesRootCertificates(ctx *chetypes.De kubeRootCertsCM.SetLabels(map[string]string{}) } - // Set TypeMeta to avoid "cause: no version "" has been registered in scheme" error - kubeRootCertsCM.TypeMeta = metav1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - } - // Add necessary labels to the ConfigMap kubeRootCertsCM.Labels[constants.KubernetesPartOfLabelKey] = constants.CheEclipseOrg kubeRootCertsCM.Labels[constants.KubernetesComponentLabelKey] = constants.CheCABundle - return deploy.SyncForClient( - client, - ctx, + err = ctx.ClusterAPI.NonCachingClientWrapper.Sync( + context.TODO(), kubeRootCertsCM, - diffs.ConfigMap([]string{constants.KubernetesPartOfLabelKey, constants.KubernetesComponentLabelKey}, nil), - ) + &k8sclient.SyncOptions{ + DiffOpts: diffs.ConfigMap( + []string{ + constants.KubernetesPartOfLabelKey, + constants.KubernetesComponentLabelKey}, + nil, + ), + }) + + return err == nil, err } func (c *CertificatesReconciler) syncOIDCIssuerCertificate(ctx *chetypes.DeployContext) (bool, error) { @@ -341,7 +365,13 @@ func (c *CertificatesReconciler) syncOIDCIssuerCertificate(ctx *chetypes.DeployC return false, err } - err := ctx.ClusterAPI.ClientWrapper.Sync(context.TODO(), cm, &k8sclient.SyncOptions{DiffOpts: diffs.ConfigMapEnsureLabels}) + err := ctx.ClusterAPI.ClientWrapper.Sync( + context.TODO(), + cm, + &k8sclient.SyncOptions{ + DiffOpts: diffs.ConfigMap(deploy.GetLabelKeys(), nil), + }, + ) return err == nil, err } @@ -378,6 +408,12 @@ func (c *CertificatesReconciler) syncCheCABundleCerts(ctx *chetypes.DeployContex } } + // Mark ConfigMap as workspace config (will be mounted in all users' containers) + labels := deploy.GetLabels(constants.WorkspacesConfig) + + // Mark as `controller.devfile.io/watch-configmap=true` to allow DWO read custom certificates + labels[dwconstants.DevWorkspaceWatchConfigMapLabel] = "true" + // Sync a new ConfigMap with all trusted CA certificates mergedCABundlesCM := &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ @@ -385,10 +421,9 @@ func (c *CertificatesReconciler) syncCheCABundleCerts(ctx *chetypes.DeployContex APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: CheMergedCABundleCertsCMName, - Namespace: ctx.CheCluster.Namespace, - // Mark ConfigMap as workspace config (will be mounted in all users' containers) - Labels: deploy.GetLabels(constants.WorkspacesConfig), + Name: CheMergedCABundleCertsCMName, + Namespace: ctx.CheCluster.Namespace, + Labels: labels, Annotations: map[string]string{}, }, Data: map[string]string{}, @@ -409,18 +444,18 @@ func (c *CertificatesReconciler) syncCheCABundleCerts(ctx *chetypes.DeployContex } mergedCABundlesCM.Annotations[dwconstants.DevWorkspaceMountAccessModeAnnotation] = "0444" - return deploy.Sync( - ctx, + if err := controllerutil.SetControllerReference(ctx.CheCluster, mergedCABundlesCM, ctx.ClusterAPI.Scheme); err != nil { + return false, err + } + + err = ctx.ClusterAPI.ClientWrapper.Sync( + context.TODO(), mergedCABundlesCM, - diffs.ConfigMap( - deploy.DefaultsLabelKeys, - []string{ - dwconstants.DevWorkspaceMountAsAnnotation, - dwconstants.DevWorkspaceMountPathAnnotation, - dwconstants.DevWorkspaceMountAccessModeAnnotation, - }, - ), + &k8sclient.SyncOptions{ + DiffOpts: diffs.ConfigMap(deploy.GetLabelsAndAnnotations(mergedCABundlesCM)), + }, ) + return err == nil, err } func readKubernetesCaBundle() ([]byte, error) { diff --git a/pkg/deploy/tls/certificates_test.go b/pkg/deploy/tls/certificates_test.go index 314f4cad5..b72abad4a 100644 --- a/pkg/deploy/tls/certificates_test.go +++ b/pkg/deploy/tls/certificates_test.go @@ -240,6 +240,7 @@ func TestSyncCheCABundleCerts(t *testing.T) { cm := &corev1.ConfigMap{} err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: CheMergedCABundleCertsCMName, Namespace: "eclipse-che"}, cm) assert.Nil(t, err) + assert.Equal(t, "true", cm.Labels[dwconstants.DevWorkspaceWatchConfigMapLabel]) cert2 := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -385,6 +386,38 @@ func TestSyncCheCABundleCertsExcludesGitHostKeyWithDefaultConfigMap(t *testing.T assert.Equal(t, expected, cm.Data[kubernetesCABundleCertsFile]) } +func TestSyncOpenShiftCABundleCertificatesRemovesInjectLabel(t *testing.T) { + existingCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.DefaultCaBundleCertsCMName, + Namespace: "eclipse-che", + Labels: map[string]string{ + constants.KubernetesPartOfLabelKey: constants.CheEclipseOrg, + constants.KubernetesComponentLabelKey: constants.CheCABundle, + constants.ConfigOpenShiftIOInjectTrustedCaBundle: "true", + }, + }, + Data: map[string]string{ + "ca-bundle.crt": "openshift-injected-ca-bundle", + }, + } + + ctx := test.NewCtxBuilder().WithObjects(existingCM).Build() + ctx.CheCluster.Spec.DevEnvironments.TrustedCerts = &chev2.TrustedCerts{DisableWorkspaceCaBundleMount: ptr.To(true)} + + test.EnsureReconcile(t, ctx, NewCertificatesReconciler().Reconcile) + + cm := &corev1.ConfigMap{} + err := ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: constants.DefaultCaBundleCertsCMName, Namespace: "eclipse-che"}, cm) + + assert.NoError(t, err) + + assert.Empty(t, cm.Labels[constants.ConfigOpenShiftIOInjectTrustedCaBundle]) + assert.Equal(t, constants.CheEclipseOrg, cm.Labels[constants.KubernetesPartOfLabelKey]) + assert.Equal(t, constants.CheCABundle, cm.Labels[constants.KubernetesComponentLabelKey]) + assert.Empty(t, cm.Data["ca-bundle.crt"]) +} + func TestToggleDisableWorkspaceCaBundleMount(t *testing.T) { // Enable workspace CA bundle mount ctx := test.NewCtxBuilder().WithObjects(&corev1.ConfigMap{