From 4bcc78a27a2ea562eb7f105c46e9dd21bdb172d7 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Fri, 21 Apr 2023 14:52:05 +0300 Subject: [PATCH] feat: Allow to configure user custom roles (#1663) * feat: Allow to configure user custom roles without duplicating default ones Signed-off-by: Anatolii Bazko --- api/checluster_conversion_from_test.go | 4 + api/checluster_conversion_to_test.go | 3 +- api/checluster_round_conversion_test.go | 6 + api/v1/checluster_conversion_from.go | 3 + api/v1/checluster_conversion_to.go | 5 + api/v2/checluster_types.go | 22 +- api/v2/zz_generated.deepcopy.go | 25 ++ .../che-operator.clusterserviceversion.yaml | 4 +- .../org.eclipse.che_checlusters.yaml | 24 +- .../bases/org.eclipse.che_checlusters.yaml | 22 +- controllers/che/checluster_controller.go | 2 - deploy/deployment/kubernetes/combined.yaml | 22 +- ....eclipse.che.CustomResourceDefinition.yaml | 22 +- deploy/deployment/openshift/combined.yaml | 22 +- ....eclipse.che.CustomResourceDefinition.yaml | 22 +- ....eclipse.che.CustomResourceDefinition.yaml | 22 +- pkg/common/constants/constants.go | 1 + pkg/common/operator-defaults/defaults.go | 27 -- pkg/common/utils/utils.go | 6 + pkg/deploy/clusterrolebinding.go | 36 -- pkg/deploy/clusterrolebinding_test.go | 62 --- pkg/deploy/finalizer.go | 14 +- pkg/deploy/finalizer_test.go | 10 - pkg/deploy/rbac/rbac.go | 78 ---- pkg/deploy/rbac/workspace_permissions.go | 387 ------------------ pkg/deploy/rbac/workspace_permissions_test.go | 102 ----- pkg/deploy/reconcile_manager.go | 19 +- pkg/deploy/reconcile_manager_test.go | 66 ++- pkg/deploy/server/rbac.go | 301 ++++++++++++++ pkg/deploy/server/rbac_test.go | 134 ++++++ pkg/deploy/server/server_configmap.go | 186 +++++---- pkg/deploy/server/server_configmap_test.go | 78 ++++ pkg/deploy/server/server_reconciler.go | 30 +- pkg/deploy/server/server_reconciler_test.go | 35 +- pkg/deploy/sync.go | 19 - pkg/deploy/sync_test.go | 23 -- 36 files changed, 929 insertions(+), 915 deletions(-) delete mode 100644 pkg/deploy/rbac/rbac.go delete mode 100644 pkg/deploy/rbac/workspace_permissions.go delete mode 100644 pkg/deploy/rbac/workspace_permissions_test.go create mode 100644 pkg/deploy/server/rbac.go create mode 100644 pkg/deploy/server/rbac_test.go diff --git a/api/checluster_conversion_from_test.go b/api/checluster_conversion_from_test.go index ca6ce7926..46315c740 100644 --- a/api/checluster_conversion_from_test.go +++ b/api/checluster_conversion_from_test.go @@ -364,6 +364,9 @@ func TestConvertFrom(t *testing.T) { SecondsOfInactivityBeforeIdling: pointer.Int32Ptr(1800), SecondsOfRunBeforeIdling: pointer.Int32Ptr(-1), MaxNumberOfRunningWorkspacesPerUser: pointer.Int64Ptr(10), + User: &chev2.UserConfiguration{ + ClusterRoles: []string{"ClusterRoles_1", "ClusterRoles_2"}, + }, }, ContainerRegistry: chev2.CheClusterContainerRegistry{ Hostname: "AirGapContainerRegistryHostname", @@ -506,6 +509,7 @@ func TestConvertFrom(t *testing.T) { }, }) assert.Equal(t, checlusterv1.Spec.Server.WorkspacesDefaultPlugins, []chev1.WorkspacesDefaultPlugins{{Editor: "Editor", Plugins: []string{"Plugins_1", "Plugins_2"}}}) + assert.Equal(t, checlusterv1.Spec.Server.CheWorkspaceClusterRole, "ClusterRoles_1,ClusterRoles_2") assert.Equal(t, checlusterv1.Spec.Storage.PvcStrategy, "PvcStrategy") assert.Equal(t, checlusterv1.Spec.Storage.PvcClaimSize, "StorageClaimSize") diff --git a/api/checluster_conversion_to_test.go b/api/checluster_conversion_to_test.go index 09351816c..f0005e4d9 100644 --- a/api/checluster_conversion_to_test.go +++ b/api/checluster_conversion_to_test.go @@ -161,7 +161,7 @@ func TestConvertTo(t *testing.T) { CheLogLevel: "CheLogLevel", CheDebug: "true", CheClusterRoles: "CheClusterRoles_1,CheClusterRoles_2", - CheWorkspaceClusterRole: "CheWorkspaceClusterRole", + CheWorkspaceClusterRole: "ClusterRoles_1,ClusterRoles_2", WorkspaceNamespaceDefault: "WorkspaceNamespaceDefault", AllowAutoProvisionUserNamespace: pointer.BoolPtr(true), WorkspaceDefaultEditor: "WorkspaceDefaultEditor", @@ -413,6 +413,7 @@ func TestConvertTo(t *testing.T) { Editor: "Editor", Plugins: []string{"Plugin_1,Plugin_2"}, }}) + assert.Equal(t, checlusterv2.Spec.DevEnvironments.User.ClusterRoles, []string{"CheClusterRoles_1", "CheClusterRoles_2"}) assert.Equal(t, checlusterv2.Spec.Components.Dashboard.Deployment.Containers[0].Name, defaults.GetCheFlavor()+"-dashboard") assert.Equal(t, checlusterv2.Spec.Components.Dashboard.Deployment.Containers[0].Image, "DashboardImage") diff --git a/api/checluster_round_conversion_test.go b/api/checluster_round_conversion_test.go index 5dc7a6a85..a13b3cbc5 100644 --- a/api/checluster_round_conversion_test.go +++ b/api/checluster_round_conversion_test.go @@ -281,6 +281,12 @@ func TestRoundConvertCheClusterV2(t *testing.T) { Effect: "Effect", }}, MaxNumberOfRunningWorkspacesPerUser: pointer.Int64Ptr(10), + User: &chev2.UserConfiguration{ + ClusterRoles: []string{ + "ClusterRoles_1", + "ClusterRoles_2", + }, + }, }, ContainerRegistry: chev2.CheClusterContainerRegistry{ Hostname: "AirGapContainerRegistryHostname", diff --git a/api/v1/checluster_conversion_from.go b/api/v1/checluster_conversion_from.go index 40f57faa7..be7bd1372 100644 --- a/api/v1/checluster_conversion_from.go +++ b/api/v1/checluster_conversion_from.go @@ -108,6 +108,9 @@ func (dst *CheCluster) convertFrom_Server(src *chev2.CheCluster) error { dst.Spec.Server.AirGapContainerRegistryHostname = src.Spec.ContainerRegistry.Hostname dst.Spec.Server.AirGapContainerRegistryOrganization = src.Spec.ContainerRegistry.Organization dst.Spec.Server.CheClusterRoles = strings.Join(src.Spec.Components.CheServer.ClusterRoles, ",") + if src.Spec.DevEnvironments.User != nil { + dst.Spec.Server.CheWorkspaceClusterRole = strings.Join(src.Spec.DevEnvironments.User.ClusterRoles, ",") + } dst.Spec.Server.CustomCheProperties = utils.CloneMap(src.Spec.Components.CheServer.ExtraProperties) if src.Spec.Components.CheServer.Debug != nil { dst.Spec.Server.CheDebug = strconv.FormatBool(*src.Spec.Components.CheServer.Debug) diff --git a/api/v1/checluster_conversion_to.go b/api/v1/checluster_conversion_to.go index 1c854a0ea..dc13957b6 100644 --- a/api/v1/checluster_conversion_to.go +++ b/api/v1/checluster_conversion_to.go @@ -162,6 +162,11 @@ func (src *CheCluster) convertTo_DevEnvironments(dst *chev2.CheCluster) error { dst.Spec.DevEnvironments.DefaultComponents = src.Spec.Server.WorkspaceDefaultComponents dst.Spec.DevEnvironments.SecondsOfInactivityBeforeIdling = src.Spec.DevWorkspace.SecondsOfInactivityBeforeIdling dst.Spec.DevEnvironments.SecondsOfRunBeforeIdling = src.Spec.DevWorkspace.SecondsOfRunBeforeIdling + if src.Spec.Server.CheWorkspaceClusterRole != "" { + dst.Spec.DevEnvironments.User = &chev2.UserConfiguration{ + ClusterRoles: strings.Split(src.Spec.Server.CheClusterRoles, ","), + } + } if err := src.convertTo_Workspaces_Storage(dst); err != nil { return err diff --git a/api/v2/checluster_types.go b/api/v2/checluster_types.go index 61c535e71..a231d4cf2 100644 --- a/api/v2/checluster_types.go +++ b/api/v2/checluster_types.go @@ -148,6 +148,9 @@ type CheClusterDevEnvironments struct { // +kubebuilder:validation:Minimum:=-1 // +optional MaxNumberOfRunningWorkspacesPerUser *int64 `json:"maxNumberOfRunningWorkspacesPerUser,omitempty"` + // User configuration. + // +optional + User *UserConfiguration `json:"user,omitempty"` } // Che components configuration. @@ -246,13 +249,13 @@ type CheServer struct { // +optional // +kubebuilder:default:=false Debug *bool `json:"debug,omitempty"` - // ClusterRoles assigned to Che ServiceAccount. - // The defaults roles are: - // - `-cheworkspaces-namespaces-clusterrole` - // - `-cheworkspaces-clusterrole` - // - `-cheworkspaces-devworkspace-clusterrole` - // where the is the namespace where the CheCluster CRD is created. + // Additional ClusterRoles assigned to Che ServiceAccount. // Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` label. + // The defaults roles are: + // - `-cheworkspaces-clusterrole` + // - `-cheworkspaces-namespaces-clusterrole` + // - `-cheworkspaces-devworkspace-clusterrole` + // where the is the namespace where the CheCluster CR is created. // The Che Operator must already have all permissions in these ClusterRoles to grant them. // +optional ClusterRoles []string `json:"clusterRoles,omitempty"` @@ -411,6 +414,13 @@ type TrustedCerts struct { GitTrustedCertsConfigMapName string `json:"gitTrustedCertsConfigMapName,omitempty"` } +type UserConfiguration struct { + // Additional ClusterRoles assigned to the user. + // The role must have `app.kubernetes.io/part-of=che.eclipse.org` label. + // +optional + ClusterRoles []string `json:"clusterRoles,omitempty"` +} + // Configuration settings related to the workspaces persistent storage. type WorkspaceStorage struct { // PVC settings when using the `per-user` PVC strategy. diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 668147350..7682a8678 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -223,6 +223,11 @@ func (in *CheClusterDevEnvironments) DeepCopyInto(out *CheClusterDevEnvironments *out = new(int64) **out = **in } + if in.User != nil { + in, out := &in.User, &out.User + *out = new(UserConfiguration) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterDevEnvironments. @@ -883,6 +888,26 @@ func (in *TrustedCerts) DeepCopy() *TrustedCerts { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserConfiguration) DeepCopyInto(out *UserConfiguration) { + *out = *in + if in.ClusterRoles != nil { + in, out := &in.ClusterRoles, &out.ClusterRoles + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserConfiguration. +func (in *UserConfiguration) DeepCopy() *UserConfiguration { + if in == nil { + return nil + } + out := new(UserConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceDefaultPlugins) DeepCopyInto(out *WorkspaceDefaultPlugins) { *out = *in diff --git a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml index a9f5b6d6d..c34e64b88 100644 --- a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml +++ b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml @@ -77,7 +77,7 @@ metadata: operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/eclipse-che/che-operator support: Eclipse Foundation - name: eclipse-che.v7.64.0-786.next + name: eclipse-che.v7.65.0-789.next namespace: placeholder spec: apiservicedefinitions: {} @@ -1243,7 +1243,7 @@ spec: minKubeVersion: 1.19.0 provider: name: Eclipse Foundation - version: 7.64.0-786.next + version: 7.65.0-789.next webhookdefinitions: - admissionReviewVersions: - v1 diff --git a/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml b/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml index 0e6d4643a..65bb0a195 100644 --- a/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml +++ b/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml @@ -4067,13 +4067,14 @@ spec: server. properties: clusterRoles: - description: 'ClusterRoles assigned to Che ServiceAccount. - The defaults roles are: - `-cheworkspaces-namespaces-clusterrole` - - `-cheworkspaces-clusterrole` - `-cheworkspaces-devworkspace-clusterrole` + description: 'Additional ClusterRoles assigned to Che ServiceAccount. + Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` + label. The defaults roles are: - `-cheworkspaces-clusterrole` + - `-cheworkspaces-namespaces-clusterrole` + - `-cheworkspaces-devworkspace-clusterrole` where the is the namespace where the CheCluster - CRD is created. Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` - label. The Che Operator must already have all permissions - in these ClusterRoles to grant them.' + CR is created. The Che Operator must already have all + permissions in these ClusterRoles to grant them.' items: type: string type: array @@ -7250,6 +7251,17 @@ spec: label.' type: string type: object + user: + description: User configuration. + properties: + clusterRoles: + description: Additional ClusterRoles assigned to the user. + The role must have `app.kubernetes.io/part-of=che.eclipse.org` + label. + items: + type: string + type: array + type: object type: object gitServices: description: A configuration that allows users to work with remote diff --git a/config/crd/bases/org.eclipse.che_checlusters.yaml b/config/crd/bases/org.eclipse.che_checlusters.yaml index 8da7571fb..7726af594 100644 --- a/config/crd/bases/org.eclipse.che_checlusters.yaml +++ b/config/crd/bases/org.eclipse.che_checlusters.yaml @@ -3938,12 +3938,13 @@ spec: server. properties: clusterRoles: - description: 'ClusterRoles assigned to Che ServiceAccount. - The defaults roles are: - `-cheworkspaces-namespaces-clusterrole` - - `-cheworkspaces-clusterrole` - `-cheworkspaces-devworkspace-clusterrole` + description: 'Additional ClusterRoles assigned to Che ServiceAccount. + Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` + label. The defaults roles are: - `-cheworkspaces-clusterrole` + - `-cheworkspaces-namespaces-clusterrole` + - `-cheworkspaces-devworkspace-clusterrole` where the is the namespace where the CheCluster - CRD is created. Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` - label. The Che Operator must already have all permissions + CR is created. The Che Operator must already have all permissions in these ClusterRoles to grant them.' items: type: string @@ -7051,6 +7052,17 @@ spec: label.' type: string type: object + user: + description: User configuration. + properties: + clusterRoles: + description: Additional ClusterRoles assigned to the user. + The role must have `app.kubernetes.io/part-of=che.eclipse.org` + label. + items: + type: string + type: array + type: object type: object gitServices: description: A configuration that allows users to work with remote diff --git a/controllers/che/checluster_controller.go b/controllers/che/checluster_controller.go index c31f26c42..b4faec7b8 100644 --- a/controllers/che/checluster_controller.go +++ b/controllers/che/checluster_controller.go @@ -100,9 +100,7 @@ func NewReconciler( reconcileManager.RegisterReconciler(tls.NewCertificatesReconciler()) reconcileManager.RegisterReconciler(tls.NewTlsSecretReconciler()) reconcileManager.RegisterReconciler(devworkspaceconfig.NewDevWorkspaceConfigReconciler()) - reconcileManager.RegisterReconciler(rbac.NewCheServerPermissionsReconciler()) reconcileManager.RegisterReconciler(rbac.NewGatewayPermissionsReconciler()) - reconcileManager.RegisterReconciler(rbac.NewWorkspacePermissionsReconciler()) // we have to expose che endpoint independently of syncing other server // resources since che host is used for dashboard deployment and che config map diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index 3174ba69b..14af3bbaf 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -3957,12 +3957,13 @@ spec: server. properties: clusterRoles: - description: 'ClusterRoles assigned to Che ServiceAccount. - The defaults roles are: - `-cheworkspaces-namespaces-clusterrole` - - `-cheworkspaces-clusterrole` - `-cheworkspaces-devworkspace-clusterrole` + description: 'Additional ClusterRoles assigned to Che ServiceAccount. + Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` + label. The defaults roles are: - `-cheworkspaces-clusterrole` + - `-cheworkspaces-namespaces-clusterrole` + - `-cheworkspaces-devworkspace-clusterrole` where the is the namespace where the CheCluster - CRD is created. Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` - label. The Che Operator must already have all permissions + CR is created. The Che Operator must already have all permissions in these ClusterRoles to grant them.' items: type: string @@ -7070,6 +7071,17 @@ spec: label.' type: string type: object + user: + description: User configuration. + properties: + clusterRoles: + description: Additional ClusterRoles assigned to the user. + The role must have `app.kubernetes.io/part-of=che.eclipse.org` + label. + items: + type: string + type: array + type: object type: object gitServices: description: A configuration that allows users to work with remote diff --git a/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index b5e910f07..b5715b17a 100644 --- a/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -3952,12 +3952,13 @@ spec: server. properties: clusterRoles: - description: 'ClusterRoles assigned to Che ServiceAccount. - The defaults roles are: - `-cheworkspaces-namespaces-clusterrole` - - `-cheworkspaces-clusterrole` - `-cheworkspaces-devworkspace-clusterrole` + description: 'Additional ClusterRoles assigned to Che ServiceAccount. + Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` + label. The defaults roles are: - `-cheworkspaces-clusterrole` + - `-cheworkspaces-namespaces-clusterrole` + - `-cheworkspaces-devworkspace-clusterrole` where the is the namespace where the CheCluster - CRD is created. Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` - label. The Che Operator must already have all permissions + CR is created. The Che Operator must already have all permissions in these ClusterRoles to grant them.' items: type: string @@ -7065,6 +7066,17 @@ spec: label.' type: string type: object + user: + description: User configuration. + properties: + clusterRoles: + description: Additional ClusterRoles assigned to the user. + The role must have `app.kubernetes.io/part-of=che.eclipse.org` + label. + items: + type: string + type: array + type: object type: object gitServices: description: A configuration that allows users to work with remote diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 47d4c0d02..0e2366d30 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -3957,12 +3957,13 @@ spec: server. properties: clusterRoles: - description: 'ClusterRoles assigned to Che ServiceAccount. - The defaults roles are: - `-cheworkspaces-namespaces-clusterrole` - - `-cheworkspaces-clusterrole` - `-cheworkspaces-devworkspace-clusterrole` + description: 'Additional ClusterRoles assigned to Che ServiceAccount. + Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` + label. The defaults roles are: - `-cheworkspaces-clusterrole` + - `-cheworkspaces-namespaces-clusterrole` + - `-cheworkspaces-devworkspace-clusterrole` where the is the namespace where the CheCluster - CRD is created. Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` - label. The Che Operator must already have all permissions + CR is created. The Che Operator must already have all permissions in these ClusterRoles to grant them.' items: type: string @@ -7070,6 +7071,17 @@ spec: label.' type: string type: object + user: + description: User configuration. + properties: + clusterRoles: + description: Additional ClusterRoles assigned to the user. + The role must have `app.kubernetes.io/part-of=che.eclipse.org` + label. + items: + type: string + type: array + type: object type: object gitServices: description: A configuration that allows users to work with remote diff --git a/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index bce902ea1..4c1257e3b 100644 --- a/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -3952,12 +3952,13 @@ spec: server. properties: clusterRoles: - description: 'ClusterRoles assigned to Che ServiceAccount. - The defaults roles are: - `-cheworkspaces-namespaces-clusterrole` - - `-cheworkspaces-clusterrole` - `-cheworkspaces-devworkspace-clusterrole` + description: 'Additional ClusterRoles assigned to Che ServiceAccount. + Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` + label. The defaults roles are: - `-cheworkspaces-clusterrole` + - `-cheworkspaces-namespaces-clusterrole` + - `-cheworkspaces-devworkspace-clusterrole` where the is the namespace where the CheCluster - CRD is created. Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` - label. The Che Operator must already have all permissions + CR is created. The Che Operator must already have all permissions in these ClusterRoles to grant them.' items: type: string @@ -7065,6 +7066,17 @@ spec: label.' type: string type: object + user: + description: User configuration. + properties: + clusterRoles: + description: Additional ClusterRoles assigned to the user. + The role must have `app.kubernetes.io/part-of=che.eclipse.org` + label. + items: + type: string + type: array + type: object type: object gitServices: description: A configuration that allows users to work with remote diff --git a/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index b5e910f07..b5715b17a 100644 --- a/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -3952,12 +3952,13 @@ spec: server. properties: clusterRoles: - description: 'ClusterRoles assigned to Che ServiceAccount. - The defaults roles are: - `-cheworkspaces-namespaces-clusterrole` - - `-cheworkspaces-clusterrole` - `-cheworkspaces-devworkspace-clusterrole` + description: 'Additional ClusterRoles assigned to Che ServiceAccount. + Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` + label. The defaults roles are: - `-cheworkspaces-clusterrole` + - `-cheworkspaces-namespaces-clusterrole` + - `-cheworkspaces-devworkspace-clusterrole` where the is the namespace where the CheCluster - CRD is created. Each role must have a `app.kubernetes.io/part-of=che.eclipse.org` - label. The Che Operator must already have all permissions + CR is created. The Che Operator must already have all permissions in these ClusterRoles to grant them.' items: type: string @@ -7065,6 +7066,17 @@ spec: label.' type: string type: object + user: + description: User configuration. + properties: + clusterRoles: + description: Additional ClusterRoles assigned to the user. + The role must have `app.kubernetes.io/part-of=che.eclipse.org` + label. + items: + type: string + type: array + type: object type: object gitServices: description: A configuration that allows users to work with remote diff --git a/pkg/common/constants/constants.go b/pkg/common/constants/constants.go index 0873e5068..32d90ee2c 100644 --- a/pkg/common/constants/constants.go +++ b/pkg/common/constants/constants.go @@ -123,6 +123,7 @@ const ( CheFlavor = "che" CheEclipseOrg = "che.eclipse.org" InstallOrUpdateFailed = "InstallOrUpdateFailed" + FinalizerSuffix = "finalizers.che.eclipse.org" // DevWorkspace DevWorkspaceServiceAccountName = "devworkspace-controller-serviceaccount" diff --git a/pkg/common/operator-defaults/defaults.go b/pkg/common/operator-defaults/defaults.go index 75089d059..205e7849f 100644 --- a/pkg/common/operator-defaults/defaults.go +++ b/pkg/common/operator-defaults/defaults.go @@ -37,9 +37,6 @@ var ( defaultSingleHostGatewayConfigSidecarImage string defaultGatewayAuthenticationSidecarImage string defaultGatewayAuthorizationSidecarImage string - defaultCheWorkspacePluginBrokerMetadataImage string - defaultCheWorkspacePluginBrokerArtifactsImage string - defaultCheServerSecureExposerJwtProxyImage string defaultConsoleLinkName string defaultConsoleLinkDisplayName string defaultConsoleLinkSection string @@ -160,30 +157,6 @@ func GetDevfileRegistryImage(checluster interface{}) string { return PatchDefaultImageName(checluster, defaultDevfileRegistryImage) } -func GetCheWorkspacePluginBrokerMetadataImage(checluster interface{}) string { - if !initialized { - logrus.Fatalf("Operator defaults are not initialized.") - } - - return PatchDefaultImageName(checluster, defaultCheWorkspacePluginBrokerMetadataImage) -} - -func GetCheWorkspacePluginBrokerArtifactsImage(checluster interface{}) string { - if !initialized { - logrus.Fatalf("Operator defaults are not initialized.") - } - - return PatchDefaultImageName(checluster, defaultCheWorkspacePluginBrokerArtifactsImage) -} - -func GetCheServerSecureExposerJwtProxyImage(checluster interface{}) string { - if !initialized { - logrus.Fatalf("Operator defaults are not initialized.") - } - - return PatchDefaultImageName(checluster, defaultCheServerSecureExposerJwtProxyImage) -} - func GetGatewayImage(checluster interface{}) string { if !initialized { logrus.Fatalf("Operator defaults are not initialized.") diff --git a/pkg/common/utils/utils.go b/pkg/common/utils/utils.go index 2b2f45956..306105d8a 100644 --- a/pkg/common/utils/utils.go +++ b/pkg/common/utils/utils.go @@ -212,6 +212,12 @@ func CloneMap(m map[string]string) map[string]string { return result } +func AddMap(a map[string]string, b map[string]string) { + for k, v := range b { + a[k] = v + } +} + // Converts label map into plain string func FormatLabels(m map[string]string) string { if len(m) == 0 { diff --git a/pkg/deploy/clusterrolebinding.go b/pkg/deploy/clusterrolebinding.go index 74fbb0709..8ecdb41d7 100644 --- a/pkg/deploy/clusterrolebinding.go +++ b/pkg/deploy/clusterrolebinding.go @@ -12,8 +12,6 @@ package deploy import ( - "strings" - "github.com/eclipse-che/che-operator/pkg/common/chetypes" "github.com/eclipse-che/che-operator/pkg/common/constants" defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" @@ -21,7 +19,6 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" rbac "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" ) var ClusterRoleBindingDiffOpts = cmp.Options{ @@ -38,39 +35,6 @@ func SyncClusterRoleBindingToCluster( return Sync(deployContext, crbSpec, ClusterRoleBindingDiffOpts) } -func SyncClusterRoleBindingAndAddFinalizerToCluster( - deployContext *chetypes.DeployContext, - name string, - serviceAccountName string, - clusterRoleName string) (bool, error) { - - finalizer := GetFinalizerName(strings.ToLower(name) + ".crb") - crbSpec := getClusterRoleBindingSpec(deployContext, name, serviceAccountName, deployContext.CheCluster.Namespace, clusterRoleName) - return SyncAndAddFinalizer(deployContext, crbSpec, ClusterRoleBindingDiffOpts, finalizer) -} - -func ReconcileClusterRoleBindingFinalizer(deployContext *chetypes.DeployContext, name string) error { - if deployContext.CheCluster.DeletionTimestamp.IsZero() { - return nil - } - - finalizer := GetFinalizerName(strings.ToLower(name) + ".crb") - return DeleteObjectWithFinalizer(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{}, finalizer) -} - -func GetLegacyUniqueClusterRoleBindingName(deployContext *chetypes.DeployContext, serviceAccount string, clusterRole string) string { - return deployContext.CheCluster.Namespace + "-" + serviceAccount + "-" + clusterRole -} - -func ReconcileLegacyClusterRoleBindingFinalizer(deployContext *chetypes.DeployContext, name string) error { - if deployContext.CheCluster.DeletionTimestamp.IsZero() { - return nil - } - - finalizer := strings.ToLower(name) + ".clusterrolebinding.finalizers.che.eclipse.org" - return DeleteObjectWithFinalizer(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{}, finalizer) -} - func getClusterRoleBindingSpec( deployContext *chetypes.DeployContext, name string, diff --git a/pkg/deploy/clusterrolebinding_test.go b/pkg/deploy/clusterrolebinding_test.go index c7b6b5ce4..368bff06a 100644 --- a/pkg/deploy/clusterrolebinding_test.go +++ b/pkg/deploy/clusterrolebinding_test.go @@ -13,11 +13,9 @@ package deploy import ( "context" - "time" chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/pkg/common/chetypes" - "github.com/eclipse-che/che-operator/pkg/common/utils" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -71,63 +69,3 @@ func TestSyncClusterRoleBindingToCluster(t *testing.T) { t.Fatalf("Failed to sync crb: %v", err) } } - -func TestSyncClusterRoleBindingAndAddFinalizerToCluster(t *testing.T) { - chev2.SchemeBuilder.AddToScheme(scheme.Scheme) - rbacv1.SchemeBuilder.AddToScheme(scheme.Scheme) - cli := fake.NewFakeClientWithScheme(scheme.Scheme) - deployContext := &chetypes.DeployContext{ - CheCluster: &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - }, - ClusterAPI: chetypes.ClusterAPI{ - Client: cli, - NonCachingClient: cli, - Scheme: scheme.Scheme, - }, - } - cli.Create(context.TODO(), deployContext.CheCluster) - - done, err := SyncClusterRoleBindingAndAddFinalizerToCluster(deployContext, "test", "sa", "clusterrole-1") - if !done || err != nil { - t.Fatalf("Failed to sync crb: %v", err) - } - - if !utils.Contains(deployContext.CheCluster.Finalizers, "test.crb.finalizers.che.eclipse.org") { - t.Fatalf("Failed to add finalizer") - } - - // don't expect any deletion - err = ReconcileClusterRoleBindingFinalizer(deployContext, "test") - if err != nil { - t.Fatalf("Failed to reconcile finalizer: %v", err) - } - - actual := &rbacv1.ClusterRoleBinding{} - err = cli.Get(context.TODO(), types.NamespacedName{Name: "test"}, actual) - if err != nil { - t.Fatalf("CRD shouldn't be deleted: %v", err) - } - if !utils.Contains(deployContext.CheCluster.Finalizers, "test.crb.finalizers.che.eclipse.org") { - t.Fatalf("Finalizer shouldn't be deleted") - } - - // crb must be deleted as well as finalizer - deployContext.CheCluster.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} - err = ReconcileClusterRoleBindingFinalizer(deployContext, "test") - if err != nil { - t.Fatalf("Failed to reconcile finalizer: %v", err) - } - - actual = &rbacv1.ClusterRoleBinding{} - err = cli.Get(context.TODO(), types.NamespacedName{Name: "test"}, actual) - if err == nil { - t.Fatalf("Failed to remove crb: %v", err) - } - if utils.Contains(deployContext.CheCluster.Finalizers, "test.crb.finalizers.che.eclipse.org") { - t.Fatalf("Failed to remove finalizer") - } -} diff --git a/pkg/deploy/finalizer.go b/pkg/deploy/finalizer.go index 04790a84b..72acb2d0c 100644 --- a/pkg/deploy/finalizer.go +++ b/pkg/deploy/finalizer.go @@ -21,6 +21,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +func CleanUpAllFinalizers(ctx *chetypes.DeployContext) error { + ctx.CheCluster.Finalizers = []string{} + return ctx.ClusterAPI.Client.Update(context.TODO(), ctx.CheCluster) +} + func AppendFinalizer(deployContext *chetypes.DeployContext, finalizer string) error { if err := ReloadCheClusterCR(deployContext); err != nil { return err @@ -78,12 +83,3 @@ func DeleteObjectWithFinalizer(deployContext *chetypes.DeployContext, key client return DeleteFinalizer(deployContext, finalizer) } - -func GetFinalizerName(prefix string) string { - finalizer := prefix + ".finalizers.che.eclipse.org" - diff := len(finalizer) - 63 - if diff > 0 { - return finalizer[:len(finalizer)-diff] - } - return finalizer -} diff --git a/pkg/deploy/finalizer_test.go b/pkg/deploy/finalizer_test.go index 18aa64bea..40a9bae42 100644 --- a/pkg/deploy/finalizer_test.go +++ b/pkg/deploy/finalizer_test.go @@ -99,13 +99,3 @@ func TestDeleteFinalizer(t *testing.T) { t.Fatalf("Failed to delete finalizer: %v", err) } } - -func TestGetFinalizerNameShouldReturnStringLess64Chars(t *testing.T) { - expected := "7890123456789012345678901234567891234567.finalizers.che.eclipse" - prefix := "7890123456789012345678901234567891234567" - - actual := GetFinalizerName(prefix) - if expected != actual { - t.Fatalf("Incorrect finalizer name: %s", actual) - } -} diff --git a/pkg/deploy/rbac/rbac.go b/pkg/deploy/rbac/rbac.go deleted file mode 100644 index cf19c724e..000000000 --- a/pkg/deploy/rbac/rbac.go +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright (c) 2012-2021 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 rbac - -import ( - "strings" - - "github.com/eclipse-che/che-operator/pkg/common/chetypes" - "github.com/eclipse-che/che-operator/pkg/common/constants" - "github.com/eclipse-che/che-operator/pkg/deploy" - "github.com/sirupsen/logrus" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -type CheServerPermissionsReconciler struct { - deploy.Reconcilable -} - -func NewCheServerPermissionsReconciler() *CheServerPermissionsReconciler { - return &CheServerPermissionsReconciler{} -} - -func (c *CheServerPermissionsReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { - // Create service account "che" for che-server component. - // "che" is the one which token is used to create workspace objects. - // Notice: Also we have on more "che-workspace" SA used by plugins like exec, terminal, metrics with limited privileges. - done, err := deploy.SyncServiceAccountToCluster(ctx, constants.DefaultCheServiceAccountName) - if !done { - return reconcile.Result{Requeue: true}, false, err - } - - for _, cheClusterRole := range ctx.CheCluster.Spec.Components.CheServer.ClusterRoles { - cheClusterRole := strings.TrimSpace(cheClusterRole) - if cheClusterRole != "" { - cheClusterRoleBindingName := cheClusterRole - done, err := deploy.SyncClusterRoleBindingAndAddFinalizerToCluster(ctx, cheClusterRoleBindingName, constants.DefaultCheServiceAccountName, cheClusterRole) - if !done { - return reconcile.Result{Requeue: true}, false, err - } - } - } - - return reconcile.Result{}, true, err -} - -func (c *CheServerPermissionsReconciler) Finalize(ctx *chetypes.DeployContext) bool { - done := true - - for _, cheClusterRole := range ctx.CheCluster.Spec.Components.CheServer.ClusterRoles { - cheClusterRole := strings.TrimSpace(cheClusterRole) - if cheClusterRole != "" { - cheClusterRoleBindingName := cheClusterRole - if err := deploy.ReconcileClusterRoleBindingFinalizer(ctx, cheClusterRoleBindingName); err != nil { - done = false - logrus.Errorf("Error deleting finalizer: %v", err) - } - - // Removes any legacy CRB https://github.com/eclipse/che/issues/19506 - cheClusterRoleBindingName = deploy.GetLegacyUniqueClusterRoleBindingName(ctx, constants.DefaultCheServiceAccountName, cheClusterRole) - if err := deploy.ReconcileLegacyClusterRoleBindingFinalizer(ctx, cheClusterRoleBindingName); err != nil { - done = false - logrus.Errorf("Error deleting finalizer: %v", err) - } - } - } - - return done -} diff --git a/pkg/deploy/rbac/workspace_permissions.go b/pkg/deploy/rbac/workspace_permissions.go deleted file mode 100644 index 1a1bbe9ba..000000000 --- a/pkg/deploy/rbac/workspace_permissions.go +++ /dev/null @@ -1,387 +0,0 @@ -// -// Copyright (c) 2019-2021 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 rbac - -import ( - "fmt" - - "github.com/devfile/devworkspace-operator/pkg/infrastructure" - "github.com/eclipse-che/che-operator/pkg/common/chetypes" - "github.com/eclipse-che/che-operator/pkg/common/constants" - "github.com/eclipse-che/che-operator/pkg/deploy" - "github.com/sirupsen/logrus" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -const ( - // CheNamespaceEditorClusterRoleNameTemplate - manage namespaces "cluster role" and "clusterrolebinding" template name - CheNamespaceEditorClusterRoleNameTemplate = "%s-cheworkspaces-namespaces-clusterrole" - // CheWorkspacesClusterRoleNameTemplate - manage workspaces "cluster role" and "clusterrolebinding" template name - CheWorkspacesClusterRoleNameTemplate = "%s-cheworkspaces-clusterrole" - // DevWorkspaceClusterRoleNameTemplate - manage DevWorkspace "cluster role" and "clusterrolebinding" template name - DevWorkspaceClusterRoleNameTemplate = "%s-cheworkspaces-devworkspace-clusterrole" - - CheWorkspacesClusterPermissionsFinalizerName = "cheWorkspaces.clusterpermissions.finalizers.che.eclipse.org" - NamespacesEditorPermissionsFinalizerName = "namespaces-editor.permissions.finalizers.che.eclipse.org" - DevWorkspacePermissionsFinalizerName = "devWorkspace.permissions.finalizers.che.eclipse.org" -) - -type WorkspacePermissionsReconciler struct { - deploy.Reconcilable -} - -func NewWorkspacePermissionsReconciler() *WorkspacePermissionsReconciler { - return &WorkspacePermissionsReconciler{} -} - -func (wp *WorkspacePermissionsReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { - done, err := wp.delegateWorkspacePermissionsInTheDifferNamespaceThanChe(ctx) - if !done { - return reconcile.Result{Requeue: true}, false, err - } - - done, err = wp.delegateNamespaceEditorPermissions(ctx) - if !done { - return reconcile.Result{Requeue: true}, false, err - } - - done, err = wp.delegateDevWorkspacePermissions(ctx) - if !done { - return reconcile.Result{Requeue: true}, false, err - } - - return reconcile.Result{}, true, nil -} - -func (wp *WorkspacePermissionsReconciler) Finalize(ctx *chetypes.DeployContext) bool { - done := true - - if completed := wp.removeNamespaceEditorPermissions(ctx); !completed { - done = false - } - - if completed := wp.removeDevWorkspacePermissions(ctx); !completed { - done = false - } - - if completed := wp.removeWorkspacePermissionsInTheDifferNamespaceThanChe(ctx); !completed { - done = false - } - - return done -} - -// Create cluster roles and cluster role bindings for "che" service account. -// che-server uses "che" service account for creation new workspaces and workspace components. -// Operator will create two cluster roles: -// - "-cheworkspaces-namespaces-clusterrole" - cluster role to manage namespace(for Kubernetes platform) -// or project(for Openshift platform) for new workspace. -// - "-cheworkspaces-clusterrole" - cluster role to create and manage k8s objects required for -// workspace components. -// Notice: After permission delegation che-server will create service account "che-workspace" ITSELF with -// "exec" and "view" roles for each new workspace. -func (wp *WorkspacePermissionsReconciler) delegateWorkspacePermissionsInTheDifferNamespaceThanChe(deployContext *chetypes.DeployContext) (bool, error) { - сheWorkspacesClusterRoleName := fmt.Sprintf(CheWorkspacesClusterRoleNameTemplate, deployContext.CheCluster.Namespace) - сheWorkspacesClusterRoleBindingName := сheWorkspacesClusterRoleName - - // Create clusterrole +kubebuilder:storageversion"-cheworkspaces-namespaces-clusterrole" to create k8s components for Che workspaces. - done, err := deploy.SyncClusterRoleToCluster(deployContext, сheWorkspacesClusterRoleName, wp.getWorkspacesPolicies()) - if !done { - return false, err - } - - done, err = deploy.SyncClusterRoleBindingToCluster(deployContext, сheWorkspacesClusterRoleBindingName, constants.DefaultCheServiceAccountName, сheWorkspacesClusterRoleName) - if !done { - return false, err - } - - err = deploy.AppendFinalizer(deployContext, CheWorkspacesClusterPermissionsFinalizerName) - return err == nil, err -} - -func (wp *WorkspacePermissionsReconciler) removeWorkspacePermissionsInTheDifferNamespaceThanChe(deployContext *chetypes.DeployContext) bool { - done := true - - cheWorkspacesClusterRoleName := fmt.Sprintf(CheWorkspacesClusterRoleNameTemplate, deployContext.CheCluster.Namespace) - cheWorkspacesClusterRoleBindingName := cheWorkspacesClusterRoleName - - if _, err := deploy.Delete(deployContext, types.NamespacedName{Name: cheWorkspacesClusterRoleName}, &rbacv1.ClusterRole{}); err != nil { - done = false - logrus.Errorf("Failed to delete Che Workspaces ClusterRole '%s', cause: %v", cheWorkspacesClusterRoleName, err) - } - - if _, err := deploy.Delete(deployContext, types.NamespacedName{Name: cheWorkspacesClusterRoleBindingName}, &rbacv1.ClusterRoleBinding{}); err != nil { - done = false - logrus.Errorf("Failed to delete Che Workspace ClusterRoleBinding '%s', cause: %v", cheWorkspacesClusterRoleBindingName, err) - } - - if err := deploy.DeleteFinalizer(deployContext, CheWorkspacesClusterPermissionsFinalizerName); err != nil { - done = false - logrus.Errorf("Error deleting finalizer: %v", err) - } - - return done -} - -func (wp *WorkspacePermissionsReconciler) delegateNamespaceEditorPermissions(deployContext *chetypes.DeployContext) (bool, error) { - сheNamespaceEditorClusterRoleName := fmt.Sprintf(CheNamespaceEditorClusterRoleNameTemplate, deployContext.CheCluster.Namespace) - сheNamespaceEditorClusterRoleBindingName := сheNamespaceEditorClusterRoleName - - // Create clusterrole "-clusterrole-manage-namespaces" to manage namespace/projects for Che workspaces. - done, err := deploy.SyncClusterRoleToCluster(deployContext, сheNamespaceEditorClusterRoleName, wp.getNamespaceEditorPolicies()) - if !done { - return false, err - } - - done, err = deploy.SyncClusterRoleBindingToCluster(deployContext, сheNamespaceEditorClusterRoleBindingName, constants.DefaultCheServiceAccountName, сheNamespaceEditorClusterRoleName) - if !done { - return false, err - } - - err = deploy.AppendFinalizer(deployContext, NamespacesEditorPermissionsFinalizerName) - return err == nil, err -} - -func (wp *WorkspacePermissionsReconciler) removeNamespaceEditorPermissions(deployContext *chetypes.DeployContext) bool { - done := true - - cheNamespaceEditorClusterRoleName := fmt.Sprintf(CheNamespaceEditorClusterRoleNameTemplate, deployContext.CheCluster.Namespace) - - if _, err := deploy.Delete(deployContext, types.NamespacedName{Name: cheNamespaceEditorClusterRoleName}, &rbacv1.ClusterRole{}); err != nil { - done = false - logrus.Errorf("Failed to delete Editor ClusterRole '%s', cause: %v", cheNamespaceEditorClusterRoleName, err) - } - - if _, err := deploy.Delete(deployContext, types.NamespacedName{Name: cheNamespaceEditorClusterRoleName}, &rbacv1.ClusterRoleBinding{}); err != nil { - done = false - logrus.Errorf("Failed to delete Editor ClusterRoleBinding '%s', cause: %v", cheNamespaceEditorClusterRoleName, err) - } - - if err := deploy.DeleteFinalizer(deployContext, NamespacesEditorPermissionsFinalizerName); err != nil { - done = false - logrus.Errorf("Error deleting finalizer: %v", err) - } - - return done -} - -func (wp *WorkspacePermissionsReconciler) delegateDevWorkspacePermissions(deployContext *chetypes.DeployContext) (bool, error) { - devWorkspaceClusterRoleName := fmt.Sprintf(DevWorkspaceClusterRoleNameTemplate, deployContext.CheCluster.Namespace) - devWorkspaceClusterRoleBindingName := devWorkspaceClusterRoleName - - done, err := deploy.SyncClusterRoleToCluster(deployContext, devWorkspaceClusterRoleName, wp.getDevWorkspacePolicies()) - if !done { - return false, err - } - - done, err = deploy.SyncClusterRoleBindingToCluster(deployContext, devWorkspaceClusterRoleBindingName, constants.DefaultCheServiceAccountName, devWorkspaceClusterRoleName) - if !done { - return false, err - } - - err = deploy.AppendFinalizer(deployContext, DevWorkspacePermissionsFinalizerName) - return err == nil, err -} - -func (wp *WorkspacePermissionsReconciler) removeDevWorkspacePermissions(deployContext *chetypes.DeployContext) bool { - done := true - - devWorkspaceClusterRoleName := fmt.Sprintf(DevWorkspaceClusterRoleNameTemplate, deployContext.CheCluster.Namespace) - devWorkspaceClusterRoleBindingName := devWorkspaceClusterRoleName - - if _, err := deploy.Delete(deployContext, types.NamespacedName{Name: devWorkspaceClusterRoleName}, &rbacv1.ClusterRole{}); err != nil { - done = false - logrus.Errorf("Failed to delete DevWorkspace ClusterRole '%s', cause: %v", devWorkspaceClusterRoleName, err) - } - - if _, err := deploy.Delete(deployContext, types.NamespacedName{Name: devWorkspaceClusterRoleBindingName}, &rbacv1.ClusterRoleBinding{}); err != nil { - done = false - logrus.Errorf("Failed to delete DevWorkspace ClusterRoleBinding '%s', cause: %v", devWorkspaceClusterRoleName, err) - } - - if err := deploy.DeleteFinalizer(deployContext, DevWorkspacePermissionsFinalizerName); err != nil { - done = false - logrus.Errorf("Error deleting finalizer: %v", err) - } - - return done -} - -func (wp *WorkspacePermissionsReconciler) getDevWorkspacePolicies() []rbacv1.PolicyRule { - k8sPolicies := []rbacv1.PolicyRule{ - { - APIGroups: []string{"workspace.devfile.io"}, - Resources: []string{"devworkspaces", "devworkspacetemplates"}, - Verbs: []string{"get", "create", "delete", "list", "update", "patch", "watch"}, - }, - } - - return k8sPolicies -} - -func (wp *WorkspacePermissionsReconciler) getNamespaceEditorPolicies() []rbacv1.PolicyRule { - k8sPolicies := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"namespaces"}, - Verbs: []string{"get", "create", "update", "list"}, - }, - } - - openshiftPolicies := []rbacv1.PolicyRule{ - { - APIGroups: []string{"project.openshift.io"}, - Resources: []string{"projectrequests"}, - Verbs: []string{"create", "update"}, - }, - { - APIGroups: []string{"project.openshift.io"}, - Resources: []string{"projects"}, - Verbs: []string{"get", "list"}, - }, - } - - if infrastructure.IsOpenShift() { - return append(k8sPolicies, openshiftPolicies...) - } - return k8sPolicies -} - -func (c *WorkspacePermissionsReconciler) getWorkspacesPolicies() []rbacv1.PolicyRule { - k8sPolicies := []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"serviceaccounts"}, - Verbs: []string{"get", "watch", "create"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"pods/exec"}, - Verbs: []string{"get", "create"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"pods/log"}, - Verbs: []string{"get", "list", "watch"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"secrets"}, - Verbs: []string{"get", "list", "create", "update", "patch", "delete"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"persistentvolumeclaims"}, - Verbs: []string{"get", "list", "watch", "create", "delete"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"pods"}, - Verbs: []string{"get", "list", "watch", "create", "delete"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"services"}, - Verbs: []string{"get", "list", "create", "delete"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"configmaps"}, - Verbs: []string{"get", "list", "create", "update", "patch", "delete"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"events"}, - Verbs: []string{"watch"}, - }, - { - APIGroups: []string{"apps"}, - Resources: []string{"secrets"}, - Verbs: []string{"list"}, - }, - { - APIGroups: []string{"apps"}, - Resources: []string{"deployments"}, - Verbs: []string{"get", "list", "watch", "create", "patch", "delete"}, - }, - { - APIGroups: []string{"apps"}, - Resources: []string{"replicasets"}, - Verbs: []string{"get", "list", "patch", "delete"}, - }, - { - APIGroups: []string{"extensions"}, - Resources: []string{"ingresses"}, - Verbs: []string{"get", "list", "watch", "create", "delete"}, - }, - { - APIGroups: []string{"networking.k8s.io"}, - Resources: []string{"ingresses"}, - Verbs: []string{"get", "list", "watch", "create", "delete"}, - }, - { - APIGroups: []string{"rbac.authorization.k8s.io"}, - Resources: []string{"roles"}, - Verbs: []string{"get", "create", "update"}, - }, - { - APIGroups: []string{"rbac.authorization.k8s.io"}, - Resources: []string{"rolebindings"}, - Verbs: []string{"get", "create", "update"}, - }, - { - APIGroups: []string{"metrics.k8s.io"}, - Resources: []string{"pods", "nodes"}, - Verbs: []string{"get", "list", "watch"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"namespaces"}, - Verbs: []string{"get", "list"}, - }, - { - APIGroups: []string{""}, - Resources: []string{"events"}, - Verbs: []string{"watch", "list"}, - }, - } - openshiftPolicies := []rbacv1.PolicyRule{ - { - APIGroups: []string{"route.openshift.io"}, - Resources: []string{"routes"}, - Verbs: []string{"get", "list", "create", "delete"}, - }, - { - APIGroups: []string{"authorization.openshift.io"}, - Resources: []string{"roles"}, - Verbs: []string{"get", "create", "update"}, - }, - { - APIGroups: []string{"authorization.openshift.io"}, - Resources: []string{"rolebindings"}, - Verbs: []string{"get", "create", "update"}, - }, - { - APIGroups: []string{"project.openshift.io"}, - Resources: []string{"projects"}, - Verbs: []string{"get"}, - }, - } - - if infrastructure.IsOpenShift() { - return append(k8sPolicies, openshiftPolicies...) - } - return k8sPolicies -} diff --git a/pkg/deploy/rbac/workspace_permissions_test.go b/pkg/deploy/rbac/workspace_permissions_test.go deleted file mode 100644 index df3b0f919..000000000 --- a/pkg/deploy/rbac/workspace_permissions_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// -// Copyright (c) 2019-2021 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 rbac - -import ( - "testing" - - "github.com/devfile/devworkspace-operator/pkg/infrastructure" - 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/common/utils" - "github.com/stretchr/testify/assert" - rbac "k8s.io/api/rbac/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" -) - -func TestReconcileWorkspacePermissions(t *testing.T) { - infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) - - type testCase struct { - name string - initObjects []runtime.Object - checluster *chev2.CheCluster - } - - testCases := []testCase{ - { - name: "che-operator should delegate permission for workspaces in differ namespace than Che. WorkspaceNamespaceDefault = 'some-test-namespace'", - initObjects: []runtime.Object{}, - checluster: &chev2.CheCluster{ - ObjectMeta: v1.ObjectMeta{ - Name: "eclipse-che", - Namespace: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - DevEnvironments: chev2.CheClusterDevEnvironments{ - DefaultNamespace: chev2.DefaultNamespace{ - Template: "some-test-namespace", - }, - }, - }, - }, - }, - { - name: "che-operator should delegate permission for workspaces in differ namespace than Che. Property CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT = 'some-test-namespace'", - initObjects: []runtime.Object{}, - checluster: &chev2.CheCluster{ - ObjectMeta: v1.ObjectMeta{ - Name: "eclipse-che", - Namespace: "eclipse-che", - }, - Spec: chev2.CheClusterSpec{ - Components: chev2.CheClusterComponents{ - CheServer: chev2.CheServer{ - ExtraProperties: map[string]string{ - "CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT": "some-test-namespace", - }, - }, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - ctx := test.GetDeployContext(testCase.checluster, testCase.initObjects) - - wp := NewWorkspacePermissionsReconciler() - _, done, err := wp.Reconcile(ctx) - - assert.Nil(t, err) - assert.True(t, done) - assert.True(t, utils.Contains(ctx.CheCluster.Finalizers, CheWorkspacesClusterPermissionsFinalizerName)) - assert.True(t, utils.Contains(ctx.CheCluster.Finalizers, NamespacesEditorPermissionsFinalizerName)) - assert.True(t, utils.Contains(ctx.CheCluster.Finalizers, DevWorkspacePermissionsFinalizerName)) - - name := "eclipse-che-cheworkspaces-clusterrole" - assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRole{})) - assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{})) - - name = "eclipse-che-cheworkspaces-namespaces-clusterrole" - assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRole{})) - assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{})) - - name = "eclipse-che-cheworkspaces-devworkspace-clusterrole" - assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRole{})) - assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{})) - }) - } -} diff --git a/pkg/deploy/reconcile_manager.go b/pkg/deploy/reconcile_manager.go index 3561bf8df..b26a26b5d 100644 --- a/pkg/deploy/reconcile_manager.go +++ b/pkg/deploy/reconcile_manager.go @@ -20,6 +20,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +const Finalizer = "cluster-resources." + constants.FinalizerSuffix + type Reconcilable interface { // Reconcile object. Reconcile(ctx *chetypes.DeployContext) (result reconcile.Result, done bool, err error) @@ -46,6 +48,10 @@ func (manager *ReconcileManager) RegisterReconciler(reconciler Reconcilable) { // Reconcile all objects in a order they have been added // If reconciliation failed then CheCluster status will be updated accordingly. func (manager *ReconcileManager) ReconcileAll(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { + if err := AppendFinalizer(ctx, Finalizer); err != nil { + return reconcile.Result{}, false, err + } + for _, reconciler := range manager.reconcilers { result, done, err := reconciler.Reconcile(ctx) if err != nil { @@ -75,13 +81,18 @@ func (manager *ReconcileManager) FinalizeAll(ctx *chetypes.DeployContext) (done for _, reconciler := range manager.reconcilers { if completed := reconciler.Finalize(ctx); !completed { reconcilerName := GetObjectType(reconciler) - errMsg := fmt.Sprintf("Finalization failed for reconciler: %s", reconcilerName) - - ctx.CheCluster.Status.Message = errMsg - _ = UpdateCheCRStatus(ctx, "Message", errMsg) + ctx.CheCluster.Status.Message = fmt.Sprintf("Finalization failed for reconciler: %s", reconcilerName) + _ = UpdateCheCRStatus(ctx, "Message", ctx.CheCluster.Status.Message) done = false } } + + if done { + if err := CleanUpAllFinalizers(ctx); err != nil { + return false + } + } + return done } diff --git a/pkg/deploy/reconcile_manager_test.go b/pkg/deploy/reconcile_manager_test.go index b63fd69da..a5ace3fd7 100644 --- a/pkg/deploy/reconcile_manager_test.go +++ b/pkg/deploy/reconcile_manager_test.go @@ -24,31 +24,43 @@ import ( type TestReconcilable struct { shouldFailReconcileOnce bool - alreadyFailed bool + shouldFailFinalizerOnce bool + alreadyFailedReconcile bool + alreadyFailedFinalizer bool } -func NewTestReconcilable(shouldFailReconcileOnce bool) *TestReconcilable { - return &TestReconcilable{shouldFailReconcileOnce, false} +func NewTestReconcilable(shouldFailReconcileOnce bool, shouldFailFinalizerOnce bool) *TestReconcilable { + return &TestReconcilable{ + shouldFailReconcileOnce, + shouldFailFinalizerOnce, + false, + false} } func (tr *TestReconcilable) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { // Fails on first invocation passes on others - if !tr.alreadyFailed && tr.shouldFailReconcileOnce { - tr.alreadyFailed = true - return reconcile.Result{}, false, fmt.Errorf("Reconcile error") + if !tr.alreadyFailedReconcile && tr.shouldFailReconcileOnce { + tr.alreadyFailedReconcile = true + return reconcile.Result{}, false, fmt.Errorf("reconcile error") } else { return reconcile.Result{}, true, nil } } func (tr *TestReconcilable) Finalize(ctx *chetypes.DeployContext) bool { - return true + // Fails on first invocation passes on others + if !tr.alreadyFailedFinalizer && tr.shouldFailFinalizerOnce { + tr.alreadyFailedFinalizer = true + return false + } else { + return true + } } func TestShouldUpdateAndCleanStatus(t *testing.T) { deployContext := test.GetDeployContext(nil, []runtime.Object{}) - tr := NewTestReconcilable(true) + tr := NewTestReconcilable(true, false) rm := NewReconcileManager() rm.RegisterReconciler(tr) @@ -58,7 +70,7 @@ func TestShouldUpdateAndCleanStatus(t *testing.T) { assert.False(t, done) assert.NotNil(t, err) assert.NotEmpty(t, deployContext.CheCluster.Status.Reason) - assert.Equal(t, "Reconciler failed deploy.TestReconcilable, cause: Reconcile error", deployContext.CheCluster.Status.Message) + assert.Equal(t, "Reconciler failed deploy.TestReconcilable, cause: reconcile error", deployContext.CheCluster.Status.Message) assert.Equal(t, tr, rm.failedReconciler) _, done, err = rm.ReconcileAll(deployContext) @@ -69,3 +81,39 @@ func TestShouldUpdateAndCleanStatus(t *testing.T) { assert.Empty(t, deployContext.CheCluster.Status.Message) assert.Nil(t, rm.failedReconciler) } + +func TestShouldCleanUpAllFinalizers(t *testing.T) { + ctx := test.GetDeployContext(nil, []runtime.Object{}) + + rm := NewReconcileManager() + rm.RegisterReconciler(NewTestReconcilable(false, false)) + + _, done, err := rm.ReconcileAll(ctx) + assert.True(t, done) + assert.NoError(t, err) + assert.Equal(t, 1, len(ctx.CheCluster.Finalizers)) + + done = rm.FinalizeAll(ctx) + assert.True(t, done) + assert.Empty(t, ctx.CheCluster.Finalizers) +} + +func TestShouldNotCleanUpAllFinalizersIfFailure(t *testing.T) { + ctx := test.GetDeployContext(nil, []runtime.Object{}) + + rm := NewReconcileManager() + rm.RegisterReconciler(NewTestReconcilable(false, true)) + + _, done, err := rm.ReconcileAll(ctx) + assert.True(t, done) + assert.NoError(t, err) + assert.Equal(t, 1, len(ctx.CheCluster.Finalizers)) + + done = rm.FinalizeAll(ctx) + assert.False(t, done) + assert.Equal(t, 1, len(ctx.CheCluster.Finalizers)) + + done = rm.FinalizeAll(ctx) + assert.True(t, done) + assert.Empty(t, ctx.CheCluster.Finalizers) +} diff --git a/pkg/deploy/server/rbac.go b/pkg/deploy/server/rbac.go new file mode 100644 index 000000000..6bcf3e305 --- /dev/null +++ b/pkg/deploy/server/rbac.go @@ -0,0 +1,301 @@ +// +// 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 server + +import ( + "fmt" + "strings" + + "github.com/devfile/devworkspace-operator/pkg/infrastructure" + util "github.com/eclipse-che/che-operator/pkg/common/utils" + "github.com/sirupsen/logrus" + + "github.com/eclipse-che/che-operator/pkg/common/chetypes" + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/deploy" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" +) + +const ( + commonPermissionsTemplateName = "%s-cheworkspaces-clusterrole" + namespacePermissionsTemplateName = "%s-cheworkspaces-namespaces-clusterrole" + devWorkspacePermissionsTemplateName = "%s-cheworkspaces-devworkspace-clusterrole" +) + +// Create ClusterRole and ClusterRoleBinding for "che" service account. +// che-server uses "che" service account for creation RBAC for a user in his namespace. +func (s *CheServerReconciler) syncPermissions(ctx *chetypes.DeployContext) (bool, error) { + policies := map[string][]rbacv1.PolicyRule{ + fmt.Sprintf(commonPermissionsTemplateName, ctx.CheCluster.Namespace): s.getCommonPolicies(), + fmt.Sprintf(namespacePermissionsTemplateName, ctx.CheCluster.Namespace): s.getNamespaceEditorPolicies(), + fmt.Sprintf(devWorkspacePermissionsTemplateName, ctx.CheCluster.Namespace): s.getDevWorkspacePolicies(), + } + + for name, policy := range policies { + if done, err := deploy.SyncClusterRoleToCluster(ctx, name, policy); !done { + return false, err + } + + if done, err := deploy.SyncClusterRoleBindingToCluster(ctx, name, constants.DefaultCheServiceAccountName, name); !done { + return false, err + } + } + + for _, cheClusterRole := range ctx.CheCluster.Spec.Components.CheServer.ClusterRoles { + cheClusterRole := strings.TrimSpace(cheClusterRole) + if cheClusterRole != "" { + if done, err := deploy.SyncClusterRoleBindingToCluster(ctx, cheClusterRole, constants.DefaultCheServiceAccountName, cheClusterRole); !done { + return false, err + } + + finalizer := s.getCRBFinalizerName(cheClusterRole) + if err := deploy.AppendFinalizer(ctx, finalizer); err != nil { + return false, err + } + } + } + + // Delete abandoned CRBs + for _, finalizer := range ctx.CheCluster.Finalizers { + if strings.HasSuffix(finalizer, cheCRBFinalizerSuffix) { + cheClusterRole := strings.TrimSuffix(finalizer, cheCRBFinalizerSuffix) + if !util.Contains(ctx.CheCluster.Spec.Components.CheServer.ClusterRoles, cheClusterRole) { + if done, err := deploy.Delete(ctx, types.NamespacedName{Name: cheClusterRole}, &rbacv1.ClusterRoleBinding{}); !done { + return false, err + } + + if err := deploy.DeleteFinalizer(ctx, finalizer); err != nil { + return false, err + } + } + } + } + + return true, nil +} + +func (s *CheServerReconciler) deletePermissions(ctx *chetypes.DeployContext) bool { + names := []string{ + fmt.Sprintf(commonPermissionsTemplateName, ctx.CheCluster.Namespace), + fmt.Sprintf(namespacePermissionsTemplateName, ctx.CheCluster.Namespace), + fmt.Sprintf(devWorkspacePermissionsTemplateName, ctx.CheCluster.Namespace), + } + + done := true + + for _, name := range names { + if _, err := deploy.Delete(ctx, types.NamespacedName{Name: name}, &rbacv1.ClusterRole{}); err != nil { + done = false + logrus.Errorf("Failed to delete ClusterRole '%s', cause: %v", name, err) + } + + if _, err := deploy.Delete(ctx, types.NamespacedName{Name: name}, &rbacv1.ClusterRoleBinding{}); err != nil { + done = false + logrus.Errorf("Failed to delete ClusterRoleBinding '%s', cause: %v", name, err) + } + } + + for _, name := range ctx.CheCluster.Spec.Components.CheServer.ClusterRoles { + name := strings.TrimSpace(name) + if name != "" { + if _, err := deploy.Delete(ctx, types.NamespacedName{Name: name}, &rbacv1.ClusterRoleBinding{}); err != nil { + done = false + logrus.Errorf("Failed to delete ClusterRoleBinding '%s', cause: %v", name, err) + } + + // Removes any legacy CRB https://github.com/eclipse/che/issues/19506 + legacyName := ctx.CheCluster.Namespace + "-" + constants.DefaultCheServiceAccountName + "-" + name + if _, err := deploy.Delete(ctx, types.NamespacedName{Name: legacyName}, &rbacv1.ClusterRoleBinding{}); err != nil { + done = false + logrus.Errorf("Failed to delete ClusterRoleBinding '%s', cause: %v", legacyName, err) + } + } + } + + return done +} + +func (s *CheServerReconciler) getDevWorkspacePolicies() []rbacv1.PolicyRule { + k8sPolicies := []rbacv1.PolicyRule{ + { + APIGroups: []string{"workspace.devfile.io"}, + Resources: []string{"devworkspaces", "devworkspacetemplates"}, + Verbs: []string{"get", "create", "delete", "list", "update", "patch", "watch"}, + }, + } + + return k8sPolicies +} + +func (s *CheServerReconciler) getNamespaceEditorPolicies() []rbacv1.PolicyRule { + k8sPolicies := []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "create", "update", "list"}, + }, + } + + openshiftPolicies := []rbacv1.PolicyRule{ + { + APIGroups: []string{"project.openshift.io"}, + Resources: []string{"projectrequests"}, + Verbs: []string{"create", "update"}, + }, + { + APIGroups: []string{"project.openshift.io"}, + Resources: []string{"projects"}, + Verbs: []string{"get", "list"}, + }, + } + + if infrastructure.IsOpenShift() { + return append(k8sPolicies, openshiftPolicies...) + } + return k8sPolicies +} + +func (s *CheServerReconciler) getCommonPolicies() []rbacv1.PolicyRule { + k8sPolicies := []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"serviceaccounts"}, + Verbs: []string{"get", "watch", "create"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"pods/exec"}, + Verbs: []string{"get", "create"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"pods/log"}, + Verbs: []string{"get", "list", "watch"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"get", "list", "create", "update", "patch", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"persistentvolumeclaims"}, + Verbs: []string{"get", "list", "watch", "create", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "list", "watch", "create", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"services"}, + Verbs: []string{"get", "list", "create", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "create", "update", "patch", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"events"}, + Verbs: []string{"watch"}, + }, + { + APIGroups: []string{"apps"}, + Resources: []string{"secrets"}, + Verbs: []string{"list"}, + }, + { + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + Verbs: []string{"get", "list", "watch", "create", "patch", "delete"}, + }, + { + APIGroups: []string{"apps"}, + Resources: []string{"replicasets"}, + Verbs: []string{"get", "list", "patch", "delete"}, + }, + { + APIGroups: []string{"extensions"}, + Resources: []string{"ingresses"}, + Verbs: []string{"get", "list", "watch", "create", "delete"}, + }, + { + APIGroups: []string{"networking.k8s.io"}, + Resources: []string{"ingresses"}, + Verbs: []string{"get", "list", "watch", "create", "delete"}, + }, + { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"roles"}, + Verbs: []string{"get", "create", "update"}, + }, + { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"rolebindings"}, + Verbs: []string{"get", "create", "update"}, + }, + { + APIGroups: []string{"metrics.k8s.io"}, + Resources: []string{"pods", "nodes"}, + Verbs: []string{"get", "list", "watch"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"events"}, + Verbs: []string{"watch", "list"}, + }, + } + openshiftPolicies := []rbacv1.PolicyRule{ + { + APIGroups: []string{"route.openshift.io"}, + Resources: []string{"routes"}, + Verbs: []string{"get", "list", "create", "delete"}, + }, + { + APIGroups: []string{"authorization.openshift.io"}, + Resources: []string{"roles"}, + Verbs: []string{"get", "create", "update"}, + }, + { + APIGroups: []string{"authorization.openshift.io"}, + Resources: []string{"rolebindings"}, + Verbs: []string{"get", "create", "update"}, + }, + { + APIGroups: []string{"project.openshift.io"}, + Resources: []string{"projects"}, + Verbs: []string{"get"}, + }, + } + + if infrastructure.IsOpenShift() { + return append(k8sPolicies, openshiftPolicies...) + } + return k8sPolicies +} + +func (s *CheServerReconciler) getUserClusterRoles(ctx *chetypes.DeployContext) []string { + return []string{ + fmt.Sprintf(commonPermissionsTemplateName, ctx.CheCluster.Namespace), + fmt.Sprintf(devWorkspacePermissionsTemplateName, ctx.CheCluster.Namespace), + } +} diff --git a/pkg/deploy/server/rbac_test.go b/pkg/deploy/server/rbac_test.go new file mode 100644 index 000000000..3d8afd428 --- /dev/null +++ b/pkg/deploy/server/rbac_test.go @@ -0,0 +1,134 @@ +// +// 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 server + +import ( + "context" + "fmt" + "testing" + + "github.com/eclipse-che/che-operator/pkg/deploy" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/apimachinery/pkg/runtime" + + "github.com/devfile/devworkspace-operator/pkg/infrastructure" + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/stretchr/testify/assert" + rbac "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestSyncPermissions(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + + type testCase struct { + name string + checluster *chev2.CheCluster + } + + testCases := []testCase{ + { + name: "Test case #1", + checluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Components: chev2.CheClusterComponents{ + CheServer: chev2.CheServer{ + ClusterRoles: []string{"test-role"}, + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ctx := test.GetDeployContext(testCase.checluster, []runtime.Object{}) + + reconciler := NewCheServerReconciler() + + done, err := reconciler.syncPermissions(ctx) + assert.True(t, done) + assert.Nil(t, err) + + names := []string{ + fmt.Sprintf(commonPermissionsTemplateName, ctx.CheCluster.Namespace), + fmt.Sprintf(namespacePermissionsTemplateName, ctx.CheCluster.Namespace), + fmt.Sprintf(devWorkspacePermissionsTemplateName, ctx.CheCluster.Namespace), + } + + for _, name := range names { + assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRole{})) + assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{})) + } + assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: "test-role"}, &rbac.ClusterRoleBinding{})) + + done = reconciler.deletePermissions(ctx) + assert.True(t, done) + + for _, name := range names { + assert.False(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRole{})) + assert.False(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{})) + } + assert.False(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: "test-role"}, &rbac.ClusterRoleBinding{})) + }) + } +} + +// TestSyncClusterRoleBinding tests that CRB is deleted when no roles are specified in CR. +func TestSyncPermissionsWhenCheClusterUpdated(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + + cheCluster := &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Components: chev2.CheClusterComponents{ + CheServer: chev2.CheServer{ + ClusterRoles: []string{"test-role"}, + }, + }, + }, + } + + ctx := test.GetDeployContext(cheCluster, []runtime.Object{}) + reconciler := NewCheServerReconciler() + + done, err := reconciler.syncPermissions(ctx) + assert.True(t, done) + assert.NoError(t, err) + + err = deploy.ReloadCheClusterCR(ctx) + assert.NoError(t, err) + + assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: "test-role"}, &rbac.ClusterRoleBinding{})) + assert.Equal(t, ctx.CheCluster.Finalizers, []string{"test-role.crb.finalizers.che.eclipse.org"}) + + ctx.CheCluster.Spec.Components.CheServer.ClusterRoles = []string{} + err = ctx.ClusterAPI.Client.Update(context.TODO(), ctx.CheCluster) + assert.NoError(t, err) + + done, err = reconciler.syncPermissions(ctx) + assert.True(t, done) + assert.NoError(t, err) + + assert.False(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Namespace: "eclipse-che", Name: "test-role"}, &rbac.ClusterRoleBinding{})) + assert.Empty(t, ctx.CheCluster.Finalizers) +} diff --git a/pkg/deploy/server/server_configmap.go b/pkg/deploy/server/server_configmap.go index fdda816b1..efba7f2cc 100644 --- a/pkg/deploy/server/server_configmap.go +++ b/pkg/deploy/server/server_configmap.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2021 Red Hat, Inc. +// 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/ @@ -35,57 +35,42 @@ const ( CheConfigMapName = "che" ) -func addMap(a map[string]string, b map[string]string) { - for k, v := range b { - a[k] = v - } -} - type CheConfigMap struct { - CheHost string `json:"CHE_HOST"` - CheMultiUser string `json:"CHE_MULTIUSER"` - ChePort string `json:"CHE_PORT"` - CheApi string `json:"CHE_API"` - CheApiInternal string `json:"CHE_API_INTERNAL"` - CheWebSocketEndpoint string `json:"CHE_WEBSOCKET_ENDPOINT"` - CheWebSocketInternalEndpoint string `json:"CHE_WEBSOCKET_INTERNAL_ENDPOINT"` - CheDebugServer string `json:"CHE_DEBUG_SERVER"` - CheMetricsEnabled string `json:"CHE_METRICS_ENABLED"` - CheInfrastructureActive string `json:"CHE_INFRASTRUCTURE_ACTIVE"` - CheInfraKubernetesServiceAccountName string `json:"CHE_INFRA_KUBERNETES_SERVICE__ACCOUNT__NAME"` - CheInfraKubernetesUserClusterRoles string `json:"CHE_INFRA_KUBERNETES_USER__CLUSTER__ROLES"` - DefaultTargetNamespace string `json:"CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT"` - NamespaceCreationAllowed string `json:"CHE_INFRA_KUBERNETES_NAMESPACE_CREATION__ALLOWED"` - PvcStrategy string `json:"CHE_INFRA_KUBERNETES_PVC_STRATEGY"` - PvcClaimSize string `json:"CHE_INFRA_KUBERNETES_PVC_QUANTITY"` - WorkspacePvcStorageClassName string `json:"CHE_INFRA_KUBERNETES_PVC_STORAGE__CLASS__NAME"` - TlsSupport string `json:"CHE_INFRA_OPENSHIFT_TLS__ENABLED"` - K8STrustCerts string `json:"CHE_INFRA_KUBERNETES_TRUST__CERTS"` - CheLogLevel string `json:"CHE_LOG_LEVEL"` - IdentityProviderUrl string `json:"CHE_OIDC_AUTH__SERVER__URL,omitempty"` - IdentityProviderInternalURL string `json:"CHE_OIDC_AUTH__INTERNAL__SERVER__URL,omitempty"` - OpenShiftIdentityProvider string `json:"CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER"` - JavaOpts string `json:"JAVA_OPTS"` - WorkspaceJavaOpts string `json:"CHE_WORKSPACE_JAVA__OPTIONS"` - WorkspaceMavenOpts string `json:"CHE_WORKSPACE_MAVEN__OPTIONS"` - WorkspaceProxyJavaOpts string `json:"CHE_WORKSPACE_HTTP__PROXY__JAVA__OPTIONS"` - WorkspaceHttpProxy string `json:"CHE_WORKSPACE_HTTP__PROXY"` - WorkspaceHttpsProxy string `json:"CHE_WORKSPACE_HTTPS__PROXY"` - WorkspaceNoProxy string `json:"CHE_WORKSPACE_NO__PROXY"` - PluginRegistryUrl string `json:"CHE_WORKSPACE_PLUGIN__REGISTRY__URL,omitempty"` - PluginRegistryInternalUrl string `json:"CHE_WORKSPACE_PLUGIN__REGISTRY__INTERNAL__URL,omitempty"` - DevfileRegistryUrl string `json:"CHE_WORKSPACE_DEVFILE__REGISTRY__URL,omitempty"` - DevfileRegistryInternalUrl string `json:"CHE_WORKSPACE_DEVFILE__REGISTRY__INTERNAL__URL,omitempty"` - CheWorkspacePluginBrokerMetadataImage string `json:"CHE_WORKSPACE_PLUGIN__BROKER_METADATA_IMAGE,omitempty"` - CheWorkspacePluginBrokerArtifactsImage string `json:"CHE_WORKSPACE_PLUGIN__BROKER_ARTIFACTS_IMAGE,omitempty"` - CheServerSecureExposerJwtProxyImage string `json:"CHE_SERVER_SECURE__EXPOSER_JWTPROXY_IMAGE,omitempty"` - CheJGroupsKubernetesLabels string `json:"KUBERNETES_LABELS,omitempty"` - CheTrustedCABundlesConfigMap string `json:"CHE_TRUSTED__CA__BUNDLES__CONFIGMAP,omitempty"` - ServerStrategy string `json:"CHE_INFRA_KUBERNETES_SERVER__STRATEGY"` - WorkspaceExposure string `json:"CHE_INFRA_KUBERNETES_SINGLEHOST_WORKSPACE_EXPOSURE"` - SingleHostGatewayConfigMapLabels string `json:"CHE_INFRA_KUBERNETES_SINGLEHOST_GATEWAY_CONFIGMAP__LABELS"` - CheDevWorkspacesEnabled string `json:"CHE_DEVWORKSPACES_ENABLED"` - Http2Disable string `json:"HTTP2_DISABLE"` + CheHost string `json:"CHE_HOST"` + CheMultiUser string `json:"CHE_MULTIUSER"` + ChePort string `json:"CHE_PORT"` + CheApi string `json:"CHE_API"` + CheApiInternal string `json:"CHE_API_INTERNAL"` + CheWebSocketEndpoint string `json:"CHE_WEBSOCKET_ENDPOINT"` + CheWebSocketInternalEndpoint string `json:"CHE_WEBSOCKET_INTERNAL_ENDPOINT"` + CheDebugServer string `json:"CHE_DEBUG_SERVER"` + CheMetricsEnabled string `json:"CHE_METRICS_ENABLED"` + CheInfrastructureActive string `json:"CHE_INFRASTRUCTURE_ACTIVE"` + CheInfraKubernetesServiceAccountName string `json:"CHE_INFRA_KUBERNETES_SERVICE__ACCOUNT__NAME"` + CheInfraKubernetesUserClusterRoles string `json:"CHE_INFRA_KUBERNETES_USER__CLUSTER__ROLES"` + DefaultTargetNamespace string `json:"CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT"` + NamespaceCreationAllowed string `json:"CHE_INFRA_KUBERNETES_NAMESPACE_CREATION__ALLOWED"` + PvcStrategy string `json:"CHE_INFRA_KUBERNETES_PVC_STRATEGY"` + PvcClaimSize string `json:"CHE_INFRA_KUBERNETES_PVC_QUANTITY"` + WorkspacePvcStorageClassName string `json:"CHE_INFRA_KUBERNETES_PVC_STORAGE__CLASS__NAME"` + TlsSupport string `json:"CHE_INFRA_OPENSHIFT_TLS__ENABLED"` + K8STrustCerts string `json:"CHE_INFRA_KUBERNETES_TRUST__CERTS"` + CheLogLevel string `json:"CHE_LOG_LEVEL"` + IdentityProviderUrl string `json:"CHE_OIDC_AUTH__SERVER__URL,omitempty"` + IdentityProviderInternalURL string `json:"CHE_OIDC_AUTH__INTERNAL__SERVER__URL,omitempty"` + OpenShiftIdentityProvider string `json:"CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER"` + JavaOpts string `json:"JAVA_OPTS"` + PluginRegistryUrl string `json:"CHE_WORKSPACE_PLUGIN__REGISTRY__URL,omitempty"` + PluginRegistryInternalUrl string `json:"CHE_WORKSPACE_PLUGIN__REGISTRY__INTERNAL__URL,omitempty"` + DevfileRegistryUrl string `json:"CHE_WORKSPACE_DEVFILE__REGISTRY__URL,omitempty"` + DevfileRegistryInternalUrl string `json:"CHE_WORKSPACE_DEVFILE__REGISTRY__INTERNAL__URL,omitempty"` + CheJGroupsKubernetesLabels string `json:"KUBERNETES_LABELS,omitempty"` + CheTrustedCABundlesConfigMap string `json:"CHE_TRUSTED__CA__BUNDLES__CONFIGMAP,omitempty"` + ServerStrategy string `json:"CHE_INFRA_KUBERNETES_SERVER__STRATEGY"` + WorkspaceExposure string `json:"CHE_INFRA_KUBERNETES_SINGLEHOST_WORKSPACE_EXPOSURE"` + SingleHostGatewayConfigMapLabels string `json:"CHE_INFRA_KUBERNETES_SINGLEHOST_GATEWAY_CONFIGMAP__LABELS"` + CheDevWorkspacesEnabled string `json:"CHE_DEVWORKSPACES_ENABLED"` + Http2Disable string `json:"HTTP2_DISABLE"` } // GetCheConfigMapData gets env values from CR spec and returns a map with key:value @@ -187,47 +172,36 @@ func (s *CheServerReconciler) getCheConfigMapData(ctx *chetypes.DeployContext) ( webSocketInternalEndpoint := fmt.Sprintf("ws://%s.%s.svc:8080/api/websocket", deploy.CheServiceName, ctx.CheCluster.Namespace) webSocketEndpoint := "wss://" + ctx.CheHost + "/api/websocket" cheWorkspaceServiceAccount := "NULL" - cheUserClusterRoleNames := fmt.Sprintf("%s-cheworkspaces-clusterrole, %s-cheworkspaces-devworkspace-clusterrole", ctx.CheCluster.Namespace, ctx.CheCluster.Namespace) data := &CheConfigMap{ - CheMultiUser: "true", - CheHost: ctx.CheHost, - ChePort: "8080", - CheApi: cheAPI, - CheApiInternal: cheInternalAPI, - CheWebSocketEndpoint: webSocketEndpoint, - CheWebSocketInternalEndpoint: webSocketInternalEndpoint, - CheDebugServer: cheDebug, - CheInfrastructureActive: infra, - CheInfraKubernetesServiceAccountName: cheWorkspaceServiceAccount, - CheInfraKubernetesUserClusterRoles: cheUserClusterRoleNames, - DefaultTargetNamespace: workspaceNamespaceDefault, - NamespaceCreationAllowed: namespaceCreationAllowed, - TlsSupport: "true", - K8STrustCerts: "true", - CheLogLevel: cheLogLevel, - OpenShiftIdentityProvider: openShiftIdentityProviderId, - JavaOpts: constants.DefaultJavaOpts + " " + proxyJavaOpts, - WorkspaceJavaOpts: constants.DefaultWorkspaceJavaOpts + " " + proxyJavaOpts, - WorkspaceMavenOpts: constants.DefaultWorkspaceJavaOpts + " " + proxyJavaOpts, - WorkspaceProxyJavaOpts: proxyJavaOpts, - WorkspaceHttpProxy: ctx.Proxy.HttpProxy, - WorkspaceHttpsProxy: ctx.Proxy.HttpsProxy, - WorkspaceNoProxy: cheWorkspaceNoProxy, - PluginRegistryUrl: pluginRegistryURL, - PluginRegistryInternalUrl: pluginRegistryInternalURL, - DevfileRegistryUrl: devfileRegistryURL, - DevfileRegistryInternalUrl: devfileRegistryInternalURL, - CheWorkspacePluginBrokerMetadataImage: defaults.GetCheWorkspacePluginBrokerMetadataImage(ctx.CheCluster), - CheWorkspacePluginBrokerArtifactsImage: defaults.GetCheWorkspacePluginBrokerArtifactsImage(ctx.CheCluster), - CheServerSecureExposerJwtProxyImage: defaults.GetCheServerSecureExposerJwtProxyImage(ctx.CheCluster), - CheJGroupsKubernetesLabels: cheLabels, - CheMetricsEnabled: cheMetrics, - CheTrustedCABundlesConfigMap: deploytls.CheAllCACertsConfigMapName, - ServerStrategy: "single-host", - WorkspaceExposure: "gateway", - SingleHostGatewayConfigMapLabels: singleHostGatewayConfigMapLabels, - CheDevWorkspacesEnabled: strconv.FormatBool(true), + CheMultiUser: "true", + CheHost: ctx.CheHost, + ChePort: "8080", + CheApi: cheAPI, + CheApiInternal: cheInternalAPI, + CheWebSocketEndpoint: webSocketEndpoint, + CheWebSocketInternalEndpoint: webSocketInternalEndpoint, + CheDebugServer: cheDebug, + CheInfrastructureActive: infra, + CheInfraKubernetesServiceAccountName: cheWorkspaceServiceAccount, + DefaultTargetNamespace: workspaceNamespaceDefault, + NamespaceCreationAllowed: namespaceCreationAllowed, + TlsSupport: "true", + K8STrustCerts: "true", + CheLogLevel: cheLogLevel, + OpenShiftIdentityProvider: openShiftIdentityProviderId, + JavaOpts: constants.DefaultJavaOpts + " " + proxyJavaOpts, + PluginRegistryUrl: pluginRegistryURL, + PluginRegistryInternalUrl: pluginRegistryInternalURL, + DevfileRegistryUrl: devfileRegistryURL, + DevfileRegistryInternalUrl: devfileRegistryInternalURL, + CheJGroupsKubernetesLabels: cheLabels, + CheMetricsEnabled: cheMetrics, + CheTrustedCABundlesConfigMap: deploytls.CheAllCACertsConfigMapName, + ServerStrategy: "single-host", + WorkspaceExposure: "gateway", + SingleHostGatewayConfigMapLabels: singleHostGatewayConfigMapLabels, + CheDevWorkspacesEnabled: strconv.FormatBool(true), // Disable HTTP2 protocol. // Fix issue with creating config maps on the cluster https://issues.redhat.com/browse/CRW-2677 // The root cause is in the HTTP2 protocol support of the okttp3 library that is used by fabric8.kubernetes-client that is used by che-server @@ -255,7 +229,7 @@ func (s *CheServerReconciler) getCheConfigMapData(ctx *chetypes.DeployContext) ( "CHE_INFRA_KUBERNETES_INGRESS_PATH__TRANSFORM": "%s(.*)", } k8sCheEnv["CHE_INFRA_KUBERNETES_ENABLE__UNSUPPORTED__K8S"] = "true" - addMap(cheEnv, k8sCheEnv) + utils.AddMap(cheEnv, k8sCheEnv) } // Add TLS key and server certificate to properties since user workspaces is created in another @@ -280,10 +254,12 @@ func (s *CheServerReconciler) getCheConfigMapData(ctx *chetypes.DeployContext) ( } } - addMap(cheEnv, ctx.CheCluster.Spec.Components.CheServer.ExtraProperties) + utils.AddMap(cheEnv, ctx.CheCluster.Spec.Components.CheServer.ExtraProperties) + + s.updateUserClusterRoles(ctx, cheEnv) for _, oauthProvider := range []string{"bitbucket", "gitlab", "github", constants.AzureDevOpsOAuth} { - err := updateIntegrationServerEndpoints(ctx, cheEnv, oauthProvider) + err := s.updateIntegrationServerEndpoints(ctx, cheEnv, oauthProvider) if err != nil { return nil, err } @@ -292,7 +268,7 @@ func (s *CheServerReconciler) getCheConfigMapData(ctx *chetypes.DeployContext) ( return cheEnv, nil } -func updateIntegrationServerEndpoints(ctx *chetypes.DeployContext, cheEnv map[string]string, oauthProvider string) error { +func (s *CheServerReconciler) updateIntegrationServerEndpoints(ctx *chetypes.DeployContext, cheEnv map[string]string, oauthProvider string) error { secret, err := getOAuthConfig(ctx, oauthProvider) if secret == nil { return err @@ -319,3 +295,25 @@ func GetCheConfigMapVersion(deployContext *chetypes.DeployContext) string { } return "" } + +func (s *CheServerReconciler) updateUserClusterRoles(ctx *chetypes.DeployContext, cheEnv map[string]string) { + userClusterRoles := strings.Join(s.getUserClusterRoles(ctx), ", ") + + for _, role := range strings.Split(cheEnv["CHE_INFRA_KUBERNETES_USER__CLUSTER__ROLES"], ",") { + role := strings.TrimSpace(role) + if !strings.Contains(userClusterRoles, role) { + userClusterRoles = userClusterRoles + ", " + role + } + } + + if ctx.CheCluster.Spec.DevEnvironments.User != nil { + for _, role := range ctx.CheCluster.Spec.DevEnvironments.User.ClusterRoles { + role := strings.TrimSpace(role) + if !strings.Contains(userClusterRoles, role) { + userClusterRoles = userClusterRoles + ", " + role + } + } + } + + cheEnv["CHE_INFRA_KUBERNETES_USER__CLUSTER__ROLES"] = userClusterRoles +} diff --git a/pkg/deploy/server/server_configmap_test.go b/pkg/deploy/server/server_configmap_test.go index 6c82b2619..f40d0324b 100644 --- a/pkg/deploy/server/server_configmap_test.go +++ b/pkg/deploy/server/server_configmap_test.go @@ -493,3 +493,81 @@ func TestShouldSetUpCorrectlyInternalCheServerURL(t *testing.T) { }) } } + +func TestUpdateUserClusterRoles(t *testing.T) { + type testCase struct { + name string + cheCluster *chev2.CheCluster + expectedUserClusterRoles string + } + + testCases := []testCase{ + { + name: "Test #1", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + }, + expectedUserClusterRoles: "eclipse-che-cheworkspaces-clusterrole, eclipse-che-cheworkspaces-devworkspace-clusterrole", + }, + { + name: "Test #2", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Components: chev2.CheClusterComponents{ + CheServer: chev2.CheServer{ + ExtraProperties: map[string]string{ + "CHE_INFRA_KUBERNETES_USER__CLUSTER__ROLES": "eclipse-che-cheworkspaces-clusterrole, test-roles-1, test-roles-2", + }, + }, + }, + }, + }, + expectedUserClusterRoles: "eclipse-che-cheworkspaces-clusterrole, eclipse-che-cheworkspaces-devworkspace-clusterrole, test-roles-1, test-roles-2", + }, + { + name: "Test #3", + cheCluster: &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Components: chev2.CheClusterComponents{ + CheServer: chev2.CheServer{ + ExtraProperties: map[string]string{ + "CHE_INFRA_KUBERNETES_USER__CLUSTER__ROLES": "eclipse-che-cheworkspaces-clusterrole, test-roles-1, test-roles-2", + }, + }, + }, + DevEnvironments: chev2.CheClusterDevEnvironments{ + User: &chev2.UserConfiguration{ + ClusterRoles: []string{ + "test-roles-3", + }, + }, + }, + }, + }, + expectedUserClusterRoles: "eclipse-che-cheworkspaces-clusterrole, eclipse-che-cheworkspaces-devworkspace-clusterrole, test-roles-1, test-roles-2, test-roles-3", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ctx := test.GetDeployContext(testCase.cheCluster, []runtime.Object{}) + + reconciler := NewCheServerReconciler() + cheEnv, err := reconciler.getCheConfigMapData(ctx) + + assert.NoError(t, err) + assert.Equal(t, testCase.expectedUserClusterRoles, cheEnv["CHE_INFRA_KUBERNETES_USER__CLUSTER__ROLES"]) + }) + } +} diff --git a/pkg/deploy/server/server_reconciler.go b/pkg/deploy/server/server_reconciler.go index d621025f3..e666d1ee2 100644 --- a/pkg/deploy/server/server_reconciler.go +++ b/pkg/deploy/server/server_reconciler.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2021 Red Hat, Inc. +// 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/ @@ -14,6 +14,7 @@ package server import ( chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/pkg/common/chetypes" + "github.com/eclipse-che/che-operator/pkg/common/constants" defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" "github.com/eclipse-che/che-operator/pkg/deploy" "github.com/sirupsen/logrus" @@ -22,6 +23,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +const ( + crb = ".crb." + cheCRBFinalizerSuffix = crb + constants.FinalizerSuffix +) + type CheServerReconciler struct { deploy.Reconcilable } @@ -43,6 +49,14 @@ func (s *CheServerReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile. return reconcile.Result{}, false, err } + if done, err := deploy.SyncServiceAccountToCluster(ctx, constants.DefaultCheServiceAccountName); !done { + return reconcile.Result{}, false, err + } + + if done, err := s.syncPermissions(ctx); !done { + return reconcile.Result{}, false, err + } + done, err = s.syncDeployment(ctx) if !done { return reconcile.Result{}, false, err @@ -66,8 +80,8 @@ func (s *CheServerReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile. return reconcile.Result{}, true, nil } -func (s *CheServerReconciler) Finalize(ctx *chetypes.DeployContext) bool { - return true +func (c *CheServerReconciler) Finalize(ctx *chetypes.DeployContext) bool { + return c.deletePermissions(ctx) } func (s *CheServerReconciler) syncCheConfigMap(ctx *chetypes.DeployContext) (bool, error) { @@ -115,6 +129,15 @@ func (s *CheServerReconciler) syncActiveChePhase(ctx *chetypes.DeployContext) (b return true, nil } +func (s *CheServerReconciler) getCRBFinalizerName(crbName string) string { + finalizer := crbName + cheCRBFinalizerSuffix + diff := len(finalizer) - 63 + if diff > 0 { + return finalizer[:len(finalizer)-diff] + } + return finalizer +} + func (s *CheServerReconciler) syncDeployment(ctx *chetypes.DeployContext) (bool, error) { spec, err := s.getDeploymentSpec(ctx) if err != nil { @@ -133,6 +156,7 @@ func (s CheServerReconciler) syncCheVersion(ctx *chetypes.DeployContext) (bool, } return true, nil } + func (s CheServerReconciler) syncCheURL(ctx *chetypes.DeployContext) (bool, error) { var cheUrl = "https://" + ctx.CheHost if ctx.CheCluster.Status.CheURL != cheUrl { diff --git a/pkg/deploy/server/server_reconciler_test.go b/pkg/deploy/server/server_reconciler_test.go index f1ef985f7..09b3c3b4d 100644 --- a/pkg/deploy/server/server_reconciler_test.go +++ b/pkg/deploy/server/server_reconciler_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2021 Red Hat, Inc. +// 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/ @@ -15,13 +15,13 @@ import ( "context" "github.com/devfile/devworkspace-operator/pkg/infrastructure" + chev2 "github.com/eclipse-che/che-operator/api/v2" defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" "github.com/eclipse-che/che-operator/pkg/common/test" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - - chev2 "github.com/eclipse-che/che-operator/api/v2" + rbac "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -37,6 +37,13 @@ func TestReconcile(t *testing.T) { Namespace: "eclipse-che", Name: "eclipse-che", }, + Spec: chev2.CheClusterSpec{ + Components: chev2.CheClusterComponents{ + CheServer: chev2.CheServer{ + ClusterRoles: []string{"test-role"}, + }, + }, + }, } ctx := test.GetDeployContext(cheCluster, []runtime.Object{}) @@ -47,8 +54,11 @@ func TestReconcile(t *testing.T) { assert.Nil(t, err) assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: CheConfigMapName, Namespace: "eclipse-che"}, &corev1.ConfigMap{})) + assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Namespace: "eclipse-che", Name: "che"}, &corev1.ServiceAccount{})) + assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: "test-role"}, &rbac.ClusterRoleBinding{})) assert.True(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: getComponentName(ctx), Namespace: "eclipse-che"}, &appsv1.Deployment{})) assert.Equal(t, ctx.CheCluster.Status.ChePhase, chev2.CheClusterPhase(chev2.ClusterPhaseInactive)) + assert.Equal(t, 1, len(ctx.CheCluster.Finalizers)) cheDeployment := &appsv1.Deployment{} err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: defaults.GetCheFlavor(), Namespace: "eclipse-che"}, cheDeployment) @@ -63,8 +73,13 @@ func TestReconcile(t *testing.T) { assert.Nil(t, err) assert.Equal(t, ctx.CheCluster.Status.ChePhase, chev2.CheClusterPhase(chev2.ClusterPhaseActive)) - assert.NotEmpty(t, cheCluster.Status.CheVersion) - assert.NotEmpty(t, cheCluster.Status.CheURL) + assert.NotEmpty(t, ctx.CheCluster.Status.CheVersion) + assert.NotEmpty(t, ctx.CheCluster.Status.CheURL) + + done = server.Finalize(ctx) + assert.True(t, done) + + assert.False(t, test.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: "test-role"}, &rbac.ClusterRoleBinding{})) } func TestUpdateAvailabilityStatus(t *testing.T) { @@ -103,3 +118,13 @@ func TestUpdateAvailabilityStatus(t *testing.T) { assert.Nil(t, err) assert.Equal(t, ctx.CheCluster.Status.ChePhase, chev2.CheClusterPhase(chev2.RollingUpdate)) } + +func TestGetFinalizerName(t *testing.T) { + crbName := "0123456789012345678901234567890123456789" // 40 chars + + reconciler := NewCheServerReconciler() + finalizer := reconciler.getCRBFinalizerName(crbName) + + assert.Equal(t, crbName+".crb.finalizers.che.ecl", finalizer) + assert.True(t, len(finalizer) <= 63) +} diff --git a/pkg/deploy/sync.go b/pkg/deploy/sync.go index 1a1e7f8a5..3df6d122f 100644 --- a/pkg/deploy/sync.go +++ b/pkg/deploy/sync.go @@ -62,25 +62,6 @@ func SyncWithClient(cli client.Client, deployContext *chetypes.DeployContext, bl return UpdateWithClient(cli, deployContext, actual.(client.Object), blueprint, diffOpts...) } -func SyncAndAddFinalizer( - deployContext *chetypes.DeployContext, - blueprint metav1.Object, - diffOpts cmp.Option, - finalizer string) (bool, error) { - - // eclipse-che custom resource is being deleted, we shouldn't sync - // TODO move this check before `Sync` invocation - if deployContext.CheCluster.ObjectMeta.DeletionTimestamp.IsZero() { - done, err := Sync(deployContext, blueprint.(client.Object), diffOpts) - if !done { - return done, err - } - err = AppendFinalizer(deployContext, finalizer) - return err == nil, err - } - return true, nil -} - // Gets object by key. // Returns true if object exists otherwise returns false. func Get(deployContext *chetypes.DeployContext, key client.ObjectKey, actual client.Object) (bool, error) { diff --git a/pkg/deploy/sync_test.go b/pkg/deploy/sync_test.go index 2251e80ea..a8477be30 100644 --- a/pkg/deploy/sync_test.go +++ b/pkg/deploy/sync_test.go @@ -16,7 +16,6 @@ import ( chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/pkg/common/chetypes" - "github.com/eclipse-che/che-operator/pkg/common/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" corev1 "k8s.io/api/core/v1" @@ -171,28 +170,6 @@ func TestUpdate(t *testing.T) { } } -func TestSyncAndAddFinalizer(t *testing.T) { - cli, deployContext := initDeployContext() - - cli.Create(context.TODO(), deployContext.CheCluster) - - // Sync object - done, err := SyncAndAddFinalizer(deployContext, testObj.DeepCopy(), cmp.Options{}, "test-finalizer") - if !done || err != nil { - t.Fatalf("Error syncing object: %v", err) - } - - actual := &corev1.Secret{} - err = cli.Get(context.TODO(), testKey, actual) - if err != nil { - t.Fatalf("Failed to get object: %v", err) - } - - if !utils.Contains(deployContext.CheCluster.Finalizers, "test-finalizer") { - t.Fatalf("Failed to add finalizer") - } -} - func TestShouldDeleteExistedObject(t *testing.T) { cli, deployContext := initDeployContext()