// // Copyright (c) 2019-2023 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 usernamespace import ( "context" "encoding/json" "sync" "testing" containerbuild "github.com/eclipse-che/che-operator/pkg/deploy/container-build" rbacv1 "k8s.io/api/rbac/v1" dwconstants "github.com/devfile/devworkspace-operator/pkg/constants" "github.com/devfile/devworkspace-operator/pkg/infrastructure" devworkspaceinfra "github.com/devfile/devworkspace-operator/pkg/infrastructure" chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/controllers/devworkspace" "github.com/eclipse-che/che-operator/pkg/common/constants" "github.com/eclipse-che/che-operator/pkg/deploy/tls" configv1 "github.com/openshift/api/config/v1" projectv1 "github.com/openshift/api/project/v1" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) func setupCheCluster(t *testing.T, ctx context.Context, cl client.Client, scheme *runtime.Scheme, cheNamespaceName string, cheName string) { var cheNamespace metav1.Object if infrastructure.IsOpenShift() { cheNamespace = &projectv1.Project{} } else { cheNamespace = &corev1.Namespace{} } cheNamespace.SetName(cheNamespaceName) if err := cl.Create(ctx, cheNamespace.(client.Object)); err != nil { t.Fatal(err) } cheCluster := chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ Name: cheName, Namespace: cheNamespaceName, }, Spec: chev2.CheClusterSpec{ DevEnvironments: chev2.CheClusterDevEnvironments{ DisableContainerBuildCapabilities: pointer.BoolPtr(true), NodeSelector: map[string]string{"a": "b", "c": "d"}, Tolerations: []corev1.Toleration{ { Key: "a", Operator: corev1.TolerationOpEqual, Value: "b", }, { Key: "c", Operator: corev1.TolerationOpEqual, Value: "d", }, }, TrustedCerts: &chev2.TrustedCerts{ GitTrustedCertsConfigMapName: "che-git-self-signed-cert", }, SecondsOfInactivityBeforeIdling: pointer.Int32Ptr(1800), SecondsOfRunBeforeIdling: pointer.Int32Ptr(-1), }, Networking: chev2.CheClusterSpecNetworking{ Domain: "root-domain", }, }, Status: chev2.CheClusterStatus{ CheURL: "https://che-host", }, } if err := cl.Create(ctx, &cheCluster); err != nil { t.Fatal(err) } // also create the self-signed-certificate secret to pretend we have TLS set up cert := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: constants.DefaultSelfSignedCertificateSecretName, Namespace: cheNamespaceName, }, Data: map[string][]byte{ "ca.crt": []byte("my certificate"), "other.data": []byte("should not be copied to target ns"), }, Type: "Opaque", Immutable: pointer.BoolPtr(true), } if err := cl.Create(ctx, cert); err != nil { t.Fatal(err) } caCerts := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: tls.CheAllCACertsConfigMapName, Namespace: cheNamespaceName, }, Data: map[string]string{ "trusted1": "trusted cert 1", "trusted2": "trusted cert 2", }, } if err := cl.Create(ctx, caCerts); err != nil { t.Fatal(err) } gitTlsCredentials := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "che-git-self-signed-cert", Namespace: cheNamespaceName, }, Data: map[string]string{ "githost": "the.host.of.git", "ca.crt": "the public certificate of the.host.of.git", }, } if err := cl.Create(ctx, gitTlsCredentials); err != nil { t.Fatal(err) } r := devworkspace.New(cl, scheme) // the reconciliation needs to run twice for it to be truly finished - we're setting up finalizers etc... if _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: cheName, Namespace: cheNamespaceName}}); err != nil { t.Fatal(err) } if _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: cheName, Namespace: cheNamespaceName}}); err != nil { t.Fatal(err) } } func setup(infraType devworkspaceinfra.Type, objs ...runtime.Object) (*runtime.Scheme, client.Client, *CheUserNamespaceReconciler) { devworkspaceinfra.InitializeForTesting(infraType) devworkspace.CleanCheClusterInstancesForTest() infrastructure.InitializeForTesting(infraType) scheme := createTestScheme() cl := fake.NewFakeClientWithScheme(scheme, objs...) r := &CheUserNamespaceReconciler{ client: cl, nonCachedClient: cl, scheme: scheme, namespaceCache: &namespaceCache{ client: cl, knownNamespaces: map[string]namespaceInfo{}, lock: sync.Mutex{}, }, } return scheme, cl, r } func TestSkipsUnlabeledNamespaces(t *testing.T) { test := func(t *testing.T, infraType devworkspaceinfra.Type, namespace metav1.Object) { ctx := context.TODO() scheme, cl, r := setup(infraType, namespace.(runtime.Object)) setupCheCluster(t, ctx, cl, scheme, "che", "che") if _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: namespace.GetName()}}); err != nil { t.Fatal(err) } // no new secret or configmap should be created in the namespace ss := &corev1.SecretList{} if err := cl.List(ctx, ss, client.InNamespace(namespace.GetName())); err != nil { t.Fatal(err) } assert.True(t, len(ss.Items) == 0, "No secrets expected in the tested namespace but found %d", len(ss.Items)) cs := &corev1.ConfigMapList{} if err := cl.List(ctx, cs, client.InNamespace(namespace.GetName())); err != nil { t.Fatal(err) } assert.True(t, len(cs.Items) == 0, "No configmaps expected in the tested namespace but found %d", len(cs.Items)) } t.Run("k8s", func(t *testing.T) { test(t, devworkspaceinfra.Kubernetes, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "ns", }, }) }) t.Run("openshift", func(t *testing.T) { test(t, devworkspaceinfra.OpenShiftv4, &projectv1.Project{ ObjectMeta: metav1.ObjectMeta{ Name: "prj", }, }) }) } func TestCreatesDataInNamespace(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.Kubernetes) expectedPodTolerations, err := json.Marshal([]corev1.Toleration{ { Key: "a", Operator: corev1.TolerationOpEqual, Value: "b", }, { Key: "c", Operator: corev1.TolerationOpEqual, Value: "d", }, }) assert.NoError(t, err) test := func(t *testing.T, infraType devworkspaceinfra.Type, namespace client.Object, objs ...runtime.Object) { ctx := context.TODO() allObjs := append(objs, namespace.(runtime.Object)) scheme, cl, r := setup(infraType, allObjs...) setupCheCluster(t, ctx, cl, scheme, "eclipse-che", "che") res, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: namespace.GetName()}}) assert.NoError(t, err, "Reconciliation should have succeeded") assert.False(t, res.Requeue, "The reconciliation request should have succeeded but it is requesting a requeue") idleSettings := corev1.ConfigMap{} assert.NoError(t, cl.Get(ctx, client.ObjectKey{Name: "che-idle-settings", Namespace: namespace.GetName()}, &idleSettings)) assert.Equal(t, "env", idleSettings.GetAnnotations()[dwconstants.DevWorkspaceMountAsAnnotation], "idle settings should be annotated as mount as 'env'") assert.Equal(t, "true", idleSettings.GetLabels()[dwconstants.DevWorkspaceMountLabel], "idle settings should be labeled as mounted") assert.Equal(t, 2, len(idleSettings.Data), "Expecting 2 elements in the idle settings") assert.Equal(t, "1800", idleSettings.Data["SECONDS_OF_DW_INACTIVITY_BEFORE_IDLING"], "Unexpected idle settings") assert.Equal(t, "-1", idleSettings.Data["SECONDS_OF_DW_RUN_BEFORE_IDLING"], "Unexpected idle settings") cert := corev1.Secret{} assert.NoError(t, cl.Get(ctx, client.ObjectKey{Name: "che-server-cert", Namespace: namespace.GetName()}, &cert)) assert.Equal(t, "file", cert.GetAnnotations()[dwconstants.DevWorkspaceMountAsAnnotation], "server cert should be annotated as mount as 'file'") assert.Equal(t, "/tmp/che/secret/", cert.GetAnnotations()[dwconstants.DevWorkspaceMountPathAnnotation], "server cert annotated as mounted to an unexpected path") assert.Equal(t, "true", cert.GetLabels()[dwconstants.DevWorkspaceMountLabel], "server cert should be labeled as mounted") assert.Equal(t, 1, len(cert.Data), "Expecting just 1 element in the self-signed cert") assert.Equal(t, "my certificate", string(cert.Data["ca.crt"]), "Unexpected self-signed certificate") assert.Equal(t, corev1.SecretTypeOpaque, cert.Type, "Unexpected secret type") assert.Equal(t, true, *cert.Immutable, "Unexpected mutability of the secret") caCerts := corev1.ConfigMap{} assert.NoError(t, cl.Get(ctx, client.ObjectKey{Name: "che-trusted-ca-certs", Namespace: namespace.GetName()}, &caCerts)) assert.Equal(t, "file", caCerts.GetAnnotations()[dwconstants.DevWorkspaceMountAsAnnotation], "trusted certs should be annotated as mount as 'file'") assert.Equal(t, "/public-certs", caCerts.GetAnnotations()[dwconstants.DevWorkspaceMountPathAnnotation], "trusted certs annotated as mounted to an unexpected path") assert.Equal(t, "true", caCerts.GetLabels()[dwconstants.DevWorkspaceMountLabel], "trusted certs should be labeled as mounted") assert.Equal(t, 2, len(caCerts.Data), "Expecting exactly 2 data entries in the trusted cert config map") assert.Equal(t, "trusted cert 1", string(caCerts.Data["trusted1"]), "Unexpected trusted cert 1 value") assert.Equal(t, "trusted cert 2", string(caCerts.Data["trusted2"]), "Unexpected trusted cert 2 value") gitTlsConfig := corev1.ConfigMap{} assert.NoError(t, cl.Get(ctx, client.ObjectKey{Name: "che-git-tls-creds", Namespace: namespace.GetName()}, &gitTlsConfig)) assert.Equal(t, "true", gitTlsConfig.Labels[dwconstants.DevWorkspaceGitTLSLabel]) assert.Equal(t, "true", gitTlsConfig.Labels[dwconstants.DevWorkspaceMountLabel]) assert.Equal(t, "true", gitTlsConfig.Labels[dwconstants.DevWorkspaceWatchConfigMapLabel]) assert.Equal(t, "the.host.of.git", gitTlsConfig.Data["host"]) assert.Equal(t, "the public certificate of the.host.of.git", gitTlsConfig.Data["certificate"]) updatedNs := namespace.DeepCopyObject().(client.Object) assert.NoError(t, cl.Get(ctx, client.ObjectKeyFromObject(namespace), updatedNs)) assert.Equal(t, `{"a":"b","c":"d"}`, updatedNs.GetAnnotations()[nodeSelectorAnnotation]) assert.Equal(t, string(expectedPodTolerations), updatedNs.GetAnnotations()[podTolerationsAnnotation]) } t.Run("k8s", func(t *testing.T) { test(t, devworkspaceinfra.Kubernetes, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "ns", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid", }, }, }) }) t.Run("openshift", func(t *testing.T) { test(t, devworkspaceinfra.OpenShiftv4, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "prj", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid", }, }, }, &projectv1.Project{ ObjectMeta: metav1.ObjectMeta{ Name: "prj", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid", }, }, }, &configv1.Proxy{ ObjectMeta: metav1.ObjectMeta{ Name: "cluster", }, Spec: configv1.ProxySpec{ NoProxy: ".svc", }, Status: configv1.ProxyStatus{ NoProxy: ".svc", }, }) }) } func TestUpdateSccClusterRoleBinding(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) pr1 := &projectv1.Project{ ObjectMeta: metav1.ObjectMeta{ Name: "ns1", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid_1", }, Annotations: map[string]string{ cheUsernameAnnotation: "user_1", }, }, } ns1 := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "ns1", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid_1", }, Annotations: map[string]string{ cheUsernameAnnotation: "user_1", }, }, } cheCluster := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "eclipse-che", Namespace: "eclipse-che", }, Spec: chev2.CheClusterSpec{ DevEnvironments: chev2.CheClusterDevEnvironments{ DisableContainerBuildCapabilities: pointer.BoolPtr(false), ContainerBuildConfiguration: &chev2.ContainerBuildConfiguration{ OpenShiftSecurityContextConstraint: "container-build", }, }, Networking: chev2.CheClusterSpecNetworking{ Domain: "root-domain", }, }, Status: chev2.CheClusterStatus{ CheURL: "https://che-host", }, } allObjs := []runtime.Object{ns1, pr1, cheCluster} scheme, cl, usernamespaceReconciler := setup(infrastructure.OpenShiftv4, allObjs...) // the reconciliation needs to run twice for it to be truly finished - we're setting up finalizers etc... devworkspaceReconciler := devworkspace.New(cl, scheme) if _, err := devworkspaceReconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "eclipse-che", Namespace: "eclipse-che"}}); err != nil { t.Fatal(err) } if _, err := devworkspaceReconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "eclipse-che", Namespace: "eclipse-che"}}); err != nil { t.Fatal(err) } _, err := usernamespaceReconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: ns1.GetName()}}) assert.Nil(t, err) rb := &rbacv1.RoleBinding{} err = cl.Get(context.TODO(), types.NamespacedName{Name: containerbuild.GetUserSccRbacResourcesName(), Namespace: "ns1"}, rb) assert.Nil(t, err) assert.Equal(t, "user_1", rb.Subjects[0].Name) } func TestWatchRulesForSecretsInSameNamespace(t *testing.T) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "sec", Namespace: "ns", Labels: map[string]string{"app.kubernetes.io/component": "user-settings"}, }, } _, _, r := setup(devworkspaceinfra.Kubernetes, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "ns", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid", }, }, }, secret) ctx := context.TODO() h := r.watchRulesForSecrets(ctx) rlq := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) // Let's throw event to controller about new secret creation. h.Create(event.CreateEvent{Object: secret}, rlq) amountReconcileRequests := rlq.Len() rs, _ := rlq.Get() assert.Equal(t, 1, amountReconcileRequests) assert.Equal(t, "ns", rs.(reconcile.Request).Name) } func TestWatchRulesForConfigMapsInSameNamespace(t *testing.T) { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "cm", Namespace: "ns", Labels: map[string]string{"app.kubernetes.io/component": "user-settings"}, }, } _, _, r := setup(devworkspaceinfra.Kubernetes, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: "ns", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid", }, }, }, cm) ctx := context.TODO() h := r.watchRulesForSecrets(ctx) rlq := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) // Let's throw event to controller about new config map creation. h.Create(event.CreateEvent{Object: cm}, rlq) amountReconcileRequests := rlq.Len() rs, _ := rlq.Get() assert.Equal(t, 1, amountReconcileRequests) assert.Equal(t, "ns", rs.(reconcile.Request).Name) } func TestWatchRulesForSecretsInOtherNamespaces(t *testing.T) { secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ Kind: "Secret", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: constants.DefaultSelfSignedCertificateSecretName, Namespace: "eclipse-che", }, } _, _, r := setup(devworkspaceinfra.Kubernetes, &corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "ns1", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid1", }, }, }, &corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "ns2", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid2", }, }, }, &corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "eclipse-che", }, }, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "che", Namespace: "eclipse-che", }, }, secret) ctx := context.TODO() r.namespaceCache.ExamineNamespace(ctx, "ns1") r.namespaceCache.ExamineNamespace(ctx, "ns2") r.namespaceCache.ExamineNamespace(ctx, "eclipse-che") h := r.watchRulesForSecrets(ctx) rlq := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) // Let's throw event to controller about new secret creation. h.Create(event.CreateEvent{Object: secret}, rlq) amountReconcileRequests := rlq.Len() rs1, _ := rlq.Get() rs2, _ := rlq.Get() rs3, _ := rlq.Get() reconciles := []reconcile.Request{rs1.(reconcile.Request), rs2.(reconcile.Request), rs3.(reconcile.Request)} assert.Equal(t, 3, amountReconcileRequests) assert.Contains(t, reconciles, reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns1"}}) assert.Contains(t, reconciles, reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns2"}}) assert.Contains(t, reconciles, reconcile.Request{NamespacedName: types.NamespacedName{Name: "eclipse-che"}}) } func TestWatchRulesForConfigMapsInOtherNamespaces(t *testing.T) { cm := &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ Kind: "ConfigMap", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: tls.CheAllCACertsConfigMapName, Namespace: "eclipse-che", }, } _, _, r := setup(devworkspaceinfra.Kubernetes, &corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "ns1", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid1", }, }, }, &corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "ns2", Labels: map[string]string{ workspaceNamespaceOwnerUidLabel: "uid2", }, }, }, &corev1.Namespace{ TypeMeta: metav1.TypeMeta{ Kind: "Namespace", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "eclipse-che", }, }, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "che", Namespace: "eclipse-che", }, }, cm) ctx := context.TODO() r.namespaceCache.ExamineNamespace(ctx, "ns1") r.namespaceCache.ExamineNamespace(ctx, "ns2") r.namespaceCache.ExamineNamespace(ctx, "eclipse-che") h := r.watchRulesForConfigMaps(ctx) rlq := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) // Let's throw event to controller about new config map creation. h.Create(event.CreateEvent{Object: cm}, rlq) amountReconcileRequests := rlq.Len() rs1, _ := rlq.Get() rs2, _ := rlq.Get() rs3, _ := rlq.Get() reconciles := []reconcile.Request{rs1.(reconcile.Request), rs2.(reconcile.Request), rs3.(reconcile.Request)} assert.Equal(t, 3, amountReconcileRequests) assert.Contains(t, reconciles, reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns1"}}) assert.Contains(t, reconciles, reconcile.Request{NamespacedName: types.NamespacedName{Name: "ns2"}}) assert.Contains(t, reconciles, reconcile.Request{NamespacedName: types.NamespacedName{Name: "eclipse-che"}}) }