diff --git a/controllers/che/checluster_controller.go b/controllers/che/checluster_controller.go index 22da345693..a714ec2840 100644 --- a/controllers/che/checluster_controller.go +++ b/controllers/che/checluster_controller.go @@ -14,7 +14,6 @@ package che import ( "context" - "strings" "time" "github.com/eclipse-che/che-operator/pkg/deploy" @@ -27,6 +26,7 @@ import ( openshiftoauth "github.com/eclipse-che/che-operator/pkg/deploy/openshift-oauth" "github.com/eclipse-che/che-operator/pkg/deploy/pluginregistry" "github.com/eclipse-che/che-operator/pkg/deploy/postgres" + "github.com/eclipse-che/che-operator/pkg/deploy/rbac" "github.com/eclipse-che/che-operator/pkg/deploy/server" "github.com/eclipse-che/che-operator/pkg/deploy/tls" @@ -37,7 +37,7 @@ import ( "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - rbac "k8s.io/api/rbac/v1" + rbacv1 "k8s.io/api/rbac/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/discovery" ctrl "sigs.k8s.io/controller-runtime" @@ -99,6 +99,10 @@ func NewReconciler( reconcileManager.RegisterReconciler(openshiftoauth.NewOpenShiftOAuth(openShiftOAuthUser)) reconcileManager.RegisterReconciler(tls.NewCertificatesReconciler()) reconcileManager.RegisterReconciler(tls.NewTlsSecretReconciler()) + reconcileManager.RegisterReconciler(devworkspace.NewDevWorkspaceReconciler()) + reconcileManager.RegisterReconciler(rbac.NewCheServerPermissionsReconciler()) + reconcileManager.RegisterReconciler(rbac.NewGatewayPermissionsReconciler()) + reconcileManager.RegisterReconciler(rbac.NewWorkspacePermissionsReconciler()) return &CheClusterReconciler{ Scheme: scheme, @@ -163,11 +167,11 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { IsController: true, OwnerType: &orgv1.CheCluster{}, }). - Watches(&source.Kind{Type: &rbac.Role{}}, &handler.EnqueueRequestForOwner{ + Watches(&source.Kind{Type: &rbacv1.Role{}}, &handler.EnqueueRequestForOwner{ IsController: true, OwnerType: &orgv1.CheCluster{}, }). - Watches(&source.Kind{Type: &rbac.RoleBinding{}}, &handler.EnqueueRequestForOwner{ + Watches(&source.Kind{Type: &rbacv1.RoleBinding{}}, &handler.EnqueueRequestForOwner{ IsController: true, OwnerType: &orgv1.CheCluster{}, }). @@ -225,7 +229,6 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = r.Log.WithValues("checluster", req.NamespacedName) - tests := r.tests clusterAPI := deploy.ClusterAPI{ Client: r.client, NonCachingClient: r.nonCachedClient, @@ -308,75 +311,6 @@ func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) // TODO remove in favor of r.reconcileManager.FinalizeAll(deployContext) r.reconcileFinalizers(deployContext) - // Reconcile Dev Workspace Operator - done, err := devworkspace.ReconcileDevWorkspace(deployContext) - if !done { - if err != nil { - r.Log.Error(err, "") - } - // We should `Requeue` since we don't watch Dev Workspace controller objects - return ctrl.Result{RequeueAfter: time.Second}, err - } - - // 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(deployContext, deploy.CheServiceAccountName) - if !done { - if err != nil { - logrus.Error(err) - } - return ctrl.Result{RequeueAfter: time.Second}, err - } - - if done, err = r.reconcileGatewayPermissions(deployContext); !done { - if err != nil { - logrus.Error(err) - } - // reconcile after 1 seconds since we deal with cluster objects - return reconcile.Result{RequeueAfter: time.Second}, err - } - - done, err = r.reconcileWorkspacePermissions(deployContext) - if !done { - if err != nil { - logrus.Error(err) - } - // reconcile after 1 seconds since we deal with cluster objects - return ctrl.Result{RequeueAfter: time.Second}, err - } - - if len(checluster.Spec.Server.CheClusterRoles) > 0 { - cheClusterRoles := strings.Split(checluster.Spec.Server.CheClusterRoles, ",") - for _, cheClusterRole := range cheClusterRoles { - cheClusterRole := strings.TrimSpace(cheClusterRole) - cheClusterRoleBindingName := cheClusterRole - done, err := deploy.SyncClusterRoleBindingAndAddFinalizerToCluster(deployContext, cheClusterRoleBindingName, deploy.CheServiceAccountName, cheClusterRole) - if !tests { - if !done { - logrus.Infof("Waiting on cluster role binding '%s' to be created", cheClusterRoleBindingName) - if err != nil { - logrus.Error(err) - } - return ctrl.Result{RequeueAfter: time.Second}, err - } - } - } - } - - // If the user specified an additional cluster role to use for the Che workspace, create a role binding for it - // Use a role binding instead of a cluster role binding to keep the additional access scoped to the workspace's namespace - workspaceClusterRole := checluster.Spec.Server.CheWorkspaceClusterRole - if workspaceClusterRole != "" { - done, err := deploy.SyncRoleBindingToCluster(deployContext, "che-workspace-custom", "view", workspaceClusterRole, "ClusterRole") - if !done { - if err != nil { - logrus.Error(err) - } - return ctrl.Result{RequeueAfter: time.Second}, err - } - } - if err := r.GenerateAndSaveFields(deployContext); err != nil { _ = deploy.ReloadCheClusterCR(deployContext) return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 1}, err @@ -384,7 +318,7 @@ func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) if !deployContext.CheCluster.Spec.Database.ExternalDb { postgres := postgres.NewPostgres(deployContext) - done, err = postgres.SyncAll() + done, err := postgres.SyncAll() if !done { if err != nil { logrus.Error(err) @@ -396,7 +330,7 @@ func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) // we have to expose che endpoint independently of syncing other server // resources since che host is used for dashboard deployment and che config map server := server.NewServer(deployContext) - done, err = server.ExposeCheServiceAndEndpoint() + done, err := server.ExposeCheServiceAndEndpoint() if !done { if err != nil { logrus.Error(err) @@ -523,16 +457,6 @@ func (r *CheClusterReconciler) reconcileFinalizers(deployContext *deploy.DeployC } } - if deployContext.CheCluster.IsNativeUserModeEnabled() { - if _, err := r.reconcileGatewayPermissionsFinalizers(deployContext); err != nil { - logrus.Error(err) - } - } - - if _, err := r.reconcileWorkspacePermissionsFinalizers(deployContext); err != nil { - logrus.Error(err) - } - if err := deploy.ReconcileConsoleLinkFinalizer(deployContext); err != nil { logrus.Error(err) } @@ -543,23 +467,6 @@ func (r *CheClusterReconciler) reconcileFinalizers(deployContext *deploy.DeployC logrus.Error(err) } } - - if len(deployContext.CheCluster.Spec.Server.CheClusterRoles) > 0 { - cheClusterRoles := strings.Split(deployContext.CheCluster.Spec.Server.CheClusterRoles, ",") - for _, cheClusterRole := range cheClusterRoles { - cheClusterRole := strings.TrimSpace(cheClusterRole) - cheClusterRoleBindingName := cheClusterRole - if err := deploy.ReconcileClusterRoleBindingFinalizer(deployContext, cheClusterRoleBindingName); err != nil { - logrus.Error(err) - } - - // Removes any legacy CRB https://github.com/eclipse/che/issues/19506 - cheClusterRoleBindingName = deploy.GetLegacyUniqueClusterRoleBindingName(deployContext, deploy.CheServiceAccountName, cheClusterRole) - if err := deploy.ReconcileLegacyClusterRoleBindingFinalizer(deployContext, cheClusterRoleBindingName); err != nil { - logrus.Error(err) - } - } - } } func (r *CheClusterReconciler) GetCR(request ctrl.Request) (instance *orgv1.CheCluster, err error) { diff --git a/controllers/che/checluster_controller_test.go b/controllers/che/checluster_controller_test.go index 8e37751cf9..efd6904b97 100644 --- a/controllers/che/checluster_controller_test.go +++ b/controllers/che/checluster_controller_test.go @@ -14,17 +14,13 @@ package che import ( "context" - "fmt" "os" "strconv" - mocks "github.com/eclipse-che/che-operator/mocks" - "reflect" "time" chev1alpha1 "github.com/che-incubator/kubernetes-image-puller-operator/api/v1alpha1" - "github.com/golang/mock/gomock" crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" devworkspace "github.com/eclipse-che/che-operator/pkg/deploy/dev-workspace" @@ -47,7 +43,7 @@ import ( "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - rbac "k8s.io/api/rbac/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/utils/pointer" "k8s.io/apimachinery/pkg/api/errors" @@ -481,7 +477,7 @@ func TestCheController(t *testing.T) { } // Get the custom role binding that should have been created for the role we passed in - rb := &rbac.RoleBinding{} + rb := &rbacv1.RoleBinding{} if err := cl.Get(context.TODO(), types.NamespacedName{Name: "che-workspace-custom", Namespace: cheCR.Namespace}, rb); err != nil { t.Errorf("Custom role binding %s not found: %s", rb.Name, err) } @@ -693,135 +689,6 @@ func TestConfiguringLabelsForRoutes(t *testing.T) { } } -func TestShouldDelegatePermissionsForCheWorkspaces(t *testing.T) { - util.IsOpenShift = true - - type testCase struct { - name string - initObjects []runtime.Object - - clusterRole bool - checluster *orgv1.CheCluster - } - - // the same namespace with Che - crWsInTheSameNs1 := InitCheWithSimpleCR().DeepCopy() - crWsInTheSameNs1.Spec.Server.WorkspaceNamespaceDefault = crWsInTheSameNs1.Namespace - - crWsInTheSameNs2 := InitCheWithSimpleCR().DeepCopy() - crWsInTheSameNs2.Spec.Server.WorkspaceNamespaceDefault = "" - - crWsInTheSameNs3 := InitCheWithSimpleCR().DeepCopy() - crWsInTheSameNs3.Spec.Server.CustomCheProperties = make(map[string]string) - crWsInTheSameNs3.Spec.Server.CustomCheProperties["CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT"] = "" - - crWsInTheSameNs4 := InitCheWithSimpleCR().DeepCopy() - crWsInTheSameNs4.Spec.Server.CustomCheProperties = make(map[string]string) - crWsInTheSameNs4.Spec.Server.CustomCheProperties["CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT"] = crWsInTheSameNs1.Namespace - - // differ namespace with Che - crWsInAnotherNs1 := InitCheWithSimpleCR().DeepCopy() - crWsInAnotherNs1.Spec.Server.WorkspaceNamespaceDefault = "some-test-namespace" - - crWsInAnotherNs2 := InitCheWithSimpleCR().DeepCopy() - crWsInAnotherNs2.Spec.Server.CustomCheProperties = make(map[string]string) - crWsInAnotherNs2.Spec.Server.CustomCheProperties["CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT"] = "some-test-namespace" - - crWsInAnotherNs3 := InitCheWithSimpleCR().DeepCopy() - crWsInAnotherNs3.Spec.Server.CustomCheProperties = make(map[string]string) - crWsInAnotherNs3.Spec.Server.CustomCheProperties["CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT"] = crWsInTheSameNs1.Namespace - crWsInAnotherNs3.Spec.Server.WorkspaceNamespaceDefault = "some-test-namespace" - - testCases := []testCase{ - { - name: "che-operator should delegate permission for workspaces in differ namespace than Che. WorkspaceNamespaceDefault = 'some-test-namespace'", - initObjects: []runtime.Object{}, - clusterRole: true, - checluster: crWsInAnotherNs1, - }, - { - 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{}, - clusterRole: true, - checluster: crWsInAnotherNs2, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - logf.SetLogger(zap.New(zap.WriteTo(os.Stdout), zap.UseDevMode(true))) - - scheme := scheme.Scheme - orgv1.SchemeBuilder.AddToScheme(scheme) - scheme.AddKnownTypes(oauthv1.SchemeGroupVersion, &oauthv1.OAuthClient{}) - scheme.AddKnownTypes(userv1.SchemeGroupVersion, &userv1.UserList{}, &userv1.User{}) - scheme.AddKnownTypes(configv1.SchemeGroupVersion, &configv1.OAuth{}, &configv1.Proxy{}) - scheme.AddKnownTypes(routev1.GroupVersion, &routev1.Route{}) - - initCR := testCase.checluster - initCR.Spec.Auth.OpenShiftoAuth = pointer.BoolPtr(false) - testCase.initObjects = append(testCase.initObjects, initCR) - - cli := fake.NewFakeClientWithScheme(scheme, testCase.initObjects...) - nonCachedClient := fake.NewFakeClientWithScheme(scheme, testCase.initObjects...) - clientSet := fakeclientset.NewSimpleClientset() - // todo do we need fake discovery - fakeDiscovery, ok := clientSet.Discovery().(*fakeDiscovery.FakeDiscovery) - fakeDiscovery.Fake.Resources = []*metav1.APIResourceList{} - - if !ok { - t.Fatal("Error creating fake discovery client") - } - - var m *mocks.MockPermissionChecker - if testCase.clusterRole { - ctrl := gomock.NewController(t) - m = mocks.NewMockPermissionChecker(ctrl) - m.EXPECT().GetNotPermittedPolicyRules(gomock.Any(), "").Return([]rbac.PolicyRule{}, nil).MaxTimes(2) - defer ctrl.Finish() - } - - r := NewReconciler(cli, nonCachedClient, fakeDiscovery, scheme, "") - r.tests = true - - req := reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: os.Getenv("CHE_FLAVOR"), - Namespace: namespace, - }, - } - - _, err := r.Reconcile(context.TODO(), req) - if err != nil { - t.Fatalf("Error reconciling: %v", err) - } - _, err = r.Reconcile(context.TODO(), req) - if err != nil { - t.Fatalf("Error reconciling: %v", err) - } - - manageNamespacesClusterRoleName := fmt.Sprintf(CheNamespaceEditorClusterRoleNameTemplate, namespace) - cheManageNamespaceClusterRole := &rbac.ClusterRole{} - if err := r.nonCachedClient.Get(context.TODO(), types.NamespacedName{Name: manageNamespacesClusterRoleName}, cheManageNamespaceClusterRole); err != nil { - t.Errorf("role '%s' not found", manageNamespacesClusterRoleName) - } - cheManageNamespaceClusterRoleBinding := &rbac.ClusterRoleBinding{} - if err := r.nonCachedClient.Get(context.TODO(), types.NamespacedName{Name: manageNamespacesClusterRoleName}, cheManageNamespaceClusterRoleBinding); err != nil { - t.Errorf("rolebinding '%s' not found", manageNamespacesClusterRoleName) - } - - cheWorkspacesClusterRoleName := fmt.Sprintf(CheWorkspacesClusterRoleNameTemplate, namespace) - cheWorkspacesClusterRole := &rbac.ClusterRole{} - if err := r.nonCachedClient.Get(context.TODO(), types.NamespacedName{Name: cheWorkspacesClusterRoleName}, cheWorkspacesClusterRole); err != nil { - t.Errorf("role '%s' not found", cheWorkspacesClusterRole) - } - cheWorkspacesClusterRoleBinding := &rbac.ClusterRoleBinding{} - if err := r.nonCachedClient.Get(context.TODO(), types.NamespacedName{Name: cheWorkspacesClusterRoleName}, cheWorkspacesClusterRoleBinding); err != nil { - t.Errorf("rolebinding '%s' not found", cheWorkspacesClusterRole) - } - }) - } -} - func Init() (client.Client, discovery.DiscoveryInterface, runtime.Scheme) { objs, ds, scheme := createAPIObjects() diff --git a/controllers/che/gateway_permission.go b/controllers/che/gateway_permission.go deleted file mode 100644 index fab38fba28..0000000000 --- a/controllers/che/gateway_permission.go +++ /dev/null @@ -1,90 +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 che - -import ( - orgv1 "github.com/eclipse-che/che-operator/api/v1" - "github.com/eclipse-che/che-operator/pkg/deploy" - "github.com/eclipse-che/che-operator/pkg/deploy/gateway" - rbac "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/types" -) - -const ( - CheGatewayClusterPermissionsFinalizerName = "cheGateway.clusterpermissions.finalizers.che.eclipse.org" -) - -func (r *CheClusterReconciler) reconcileGatewayPermissions(deployContext *deploy.DeployContext) (bool, error) { - if deployContext.CheCluster.IsNativeUserModeEnabled() { - name := gatewayPermisisonsName(deployContext.CheCluster) - if _, err := deploy.SyncClusterRoleToCluster(deployContext, name, getGatewayClusterRoleRules()); err != nil { - return false, err - } - - if _, err := deploy.SyncClusterRoleBindingToCluster(deployContext, name, gateway.GatewayServiceName, name); err != nil { - return false, err - } - - if err := deploy.AppendFinalizer(deployContext, CheGatewayClusterPermissionsFinalizerName); err != nil { - return false, err - } - } else { - return deleteGatewayPermissions(deployContext) - } - - return true, nil -} - -func (r *CheClusterReconciler) reconcileGatewayPermissionsFinalizers(deployContext *deploy.DeployContext) (bool, error) { - if !deployContext.CheCluster.ObjectMeta.DeletionTimestamp.IsZero() { - return deleteGatewayPermissions(deployContext) - } - - return true, nil -} - -func deleteGatewayPermissions(deployContext *deploy.DeployContext) (bool, error) { - name := gatewayPermisisonsName(deployContext.CheCluster) - if done, err := deploy.Delete(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRole{}); !done { - return false, err - } - - if done, err := deploy.Delete(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{}); !done { - return false, err - } - - if err := deploy.DeleteFinalizer(deployContext, CheGatewayClusterPermissionsFinalizerName); err != nil { - return false, err - } - - return true, nil -} - -func gatewayPermisisonsName(instance *orgv1.CheCluster) string { - return instance.Namespace + "-" + gateway.GatewayServiceName -} - -func getGatewayClusterRoleRules() []rbac.PolicyRule { - return []rbac.PolicyRule{ - { - Verbs: []string{"create"}, - APIGroups: []string{"authentication.k8s.io"}, - Resources: []string{"tokenreviews"}, - }, - { - Verbs: []string{"create"}, - APIGroups: []string{"authorization.k8s.io"}, - Resources: []string{"subjectaccessreviews"}, - }, - } -} diff --git a/controllers/che/workspace_namespace_permission_test.go b/controllers/che/workspace_namespace_permission_test.go deleted file mode 100644 index dfcfe4a255..0000000000 --- a/controllers/che/workspace_namespace_permission_test.go +++ /dev/null @@ -1,81 +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 che - -import ( - "testing" - - "github.com/eclipse-che/che-operator/pkg/deploy" - "github.com/eclipse-che/che-operator/pkg/util" - rbac "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" -) - -func TestReconcileWorkspacePermissions(t *testing.T) { - deployContext := deploy.GetTestDeployContext(nil, []runtime.Object{}) - reconciler := &CheClusterReconciler{ - client: deployContext.ClusterAPI.Client, - nonCachedClient: deployContext.ClusterAPI.Client, - discoveryClient: deployContext.ClusterAPI.DiscoveryClient, - Scheme: deployContext.ClusterAPI.Scheme, - tests: true, - } - - done, err := reconciler.reconcileWorkspacePermissions(deployContext) - if err != nil { - t.Fatalf("Failed to reconcile permissions: %v", err) - } - if !done { - t.Fatalf("Permissions are not reconciled.") - } - - if !util.ContainsString(deployContext.CheCluster.Finalizers, CheWorkspacesClusterPermissionsFinalizerName) { - t.Fatalf("Finalizer '%s' not added", CheWorkspacesClusterPermissionsFinalizerName) - } - if !util.ContainsString(deployContext.CheCluster.Finalizers, NamespacesEditorPermissionsFinalizerName) { - t.Fatalf("Finalizer '%s' not added", NamespacesEditorPermissionsFinalizerName) - } - if !util.ContainsString(deployContext.CheCluster.Finalizers, DevWorkspacePermissionsFinalizerName) { - t.Fatalf("Finalizer '%s' not added", DevWorkspacePermissionsFinalizerName) - } - - name := "eclipse-che-cheworkspaces-clusterrole" - exists, _ := deploy.Get(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRole{}) - if !exists { - t.Fatalf("Cluster Role '%s' not found", name) - } - exists, _ = deploy.Get(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{}) - if !exists { - t.Fatalf("Cluster Role Binding '%s' not found", name) - } - - name = "eclipse-che-cheworkspaces-namespaces-clusterrole" - exists, _ = deploy.Get(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRole{}) - if !exists { - t.Fatalf("Cluster Role '%s' not found", name) - } - exists, _ = deploy.Get(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{}) - if !exists { - t.Fatalf("Cluster Role Binding '%s' not found", name) - } - - name = "eclipse-che-cheworkspaces-devworkspace-clusterrole" - exists, _ = deploy.Get(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRole{}) - if !exists { - t.Fatalf("Cluster Role '%s' not found", name) - } - exists, _ = deploy.Get(deployContext, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{}) - if !exists { - t.Fatalf("Cluster Role Binding '%s' not found", name) - } -} diff --git a/pkg/deploy/dev-workspace/object_cache.go b/pkg/deploy/dev-workspace/cache.go similarity index 93% rename from pkg/deploy/dev-workspace/object_cache.go rename to pkg/deploy/dev-workspace/cache.go index eae945baef..856490ebfc 100644 --- a/pkg/deploy/dev-workspace/object_cache.go +++ b/pkg/deploy/dev-workspace/cache.go @@ -21,14 +21,14 @@ import ( "sigs.k8s.io/yaml" ) -type CachedObjFile struct { +type DevWorkspaceCachedObj struct { data []byte hash256 string } var ( // cachedObjects - cachedObjFiles = make(map[string]*CachedObjFile) + cachedObjFiles = make(map[string]*DevWorkspaceCachedObj) ) // readK8SObject reads DWO related object from file system and cache value to avoid read later @@ -73,7 +73,7 @@ func readObj(yamlFile string, into interface{}) (string, error) { return "", err } - cachedFile = &CachedObjFile{ + cachedFile = &DevWorkspaceCachedObj{ data, util.ComputeHash256(data), } diff --git a/pkg/deploy/dev-workspace/dev_workspace.go b/pkg/deploy/dev-workspace/dev_workspace.go index 46e4fd428b..a31e5e9902 100644 --- a/pkg/deploy/dev-workspace/dev_workspace.go +++ b/pkg/deploy/dev-workspace/dev_workspace.go @@ -16,24 +16,19 @@ import ( "context" "fmt" "os" - "strings" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" - orgv1 "github.com/eclipse-che/che-operator/api/v1" "github.com/eclipse-che/che-operator/pkg/deploy" "github.com/eclipse-che/che-operator/pkg/util" - operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/sirupsen/logrus" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -var ( +const ( DevWorkspaceNamespace = "devworkspace-controller" DevWorkspaceDeploymentName = "devworkspace-controller-manager" DevWorkspaceWebhookName = "controller.devfile.io" @@ -55,43 +50,50 @@ var ( } ) -func ReconcileDevWorkspace(deployContext *deploy.DeployContext) (done bool, err error) { +type DevWorkspaceReconciler struct { + deploy.Reconcilable +} + +func NewDevWorkspaceReconciler() *DevWorkspaceReconciler { + return &DevWorkspaceReconciler{} +} + +func (d *DevWorkspaceReconciler) Reconcile(ctx *deploy.DeployContext) (reconcile.Result, bool, error) { if util.IsOpenShift && !util.IsOpenShift4 { // OpenShift 3.x is not supported - return true, nil + return reconcile.Result{}, true, nil } - if !deployContext.CheCluster.Spec.DevWorkspace.Enable { + if !ctx.CheCluster.Spec.DevWorkspace.Enable { // Do nothing if DevWorkspace is disabled - return true, nil + return reconcile.Result{}, true, nil } - if isDevWorkspaceOperatorCSVExists(deployContext) { + if isDevWorkspaceOperatorCSVExists(ctx) { // Do nothing if DevWorkspace has been already deployed via OLM - return true, nil + return reconcile.Result{}, true, nil } if util.IsOpenShift { - // - wtoInstalled, err := doesWebTerminalSubscriptionExist(deployContext) + wtoInstalled, err := isWebTerminalSubscriptionExist(ctx) if err != nil { - return false, err + return reconcile.Result{Requeue: true}, false, err } if wtoInstalled { // Do nothing if WTO exists since it should bring or embeds DWO - return true, nil + return reconcile.Result{}, true, nil } } if !deploy.IsDevWorkspaceEngineAllowed() { // Note: When the tech-preview-stable-all-namespaces will be by default stable-all-namespaces 7.40.0?, change the channel from the log - exists, err := isDevWorkspaceDeploymentExists(deployContext) + exists, err := isDevWorkspaceDeploymentExists(ctx) if err != nil { - return false, err + return reconcile.Result{Requeue: true}, false, err } if !exists { // Don't allow to deploy a new DevWorkspace operator - return false, fmt.Errorf("To enable DevWorkspace engine, deploy Eclipse Che from tech-preview channel.") + return reconcile.Result{}, false, fmt.Errorf("To enable DevWorkspace engine, deploy Eclipse Che from tech-preview channel.") } // Allow existed Eclipse Che and DevWorkspace deployments to work @@ -99,52 +101,52 @@ func ReconcileDevWorkspace(deployContext *deploy.DeployContext) (done bool, err logrus.Warnf("To enable DevWorkspace engine, deploy Eclipse Che from tech-preview channel.") } - isCreated, err := createDwNamespace(deployContext) + isCreated, err := createDwNamespace(ctx) if err != nil { - return false, err + return reconcile.Result{Requeue: true}, false, err } if !isCreated { namespace := &corev1.Namespace{} - err := deployContext.ClusterAPI.NonCachingClient.Get(context.TODO(), types.NamespacedName{Name: DevWorkspaceNamespace}, namespace) + err := ctx.ClusterAPI.NonCachingClient.Get(context.TODO(), types.NamespacedName{Name: DevWorkspaceNamespace}, namespace) if err != nil { - return false, err + return reconcile.Result{Requeue: true}, false, err } namespaceOwnershipAnnotation := namespace.GetAnnotations()[deploy.CheEclipseOrgNamespace] if namespaceOwnershipAnnotation == "" { // don't manage DWO if namespace is create by someone else not but not Che Operator - return true, err + return reconcile.Result{}, true, nil } // if DWO is managed by another Che, check if we should take control under it after possible removal - if namespaceOwnershipAnnotation != deployContext.CheCluster.Namespace { - isOnlyOneOperatorManagesDWResources, err := isOnlyOneOperatorManagesDWResources(deployContext) + if namespaceOwnershipAnnotation != ctx.CheCluster.Namespace { + isOnlyOneOperatorManagesDWResources, err := isOnlyOneOperatorManagesDWResources(ctx) if err != nil { - return false, err + return reconcile.Result{Requeue: true}, false, err } if !isOnlyOneOperatorManagesDWResources { // Don't take a control over DWO if CheCluster in another CR is handling it - return true, nil + return reconcile.Result{}, true, nil } - namespace.GetAnnotations()[deploy.CheEclipseOrgNamespace] = deployContext.CheCluster.Namespace - _, err = deploy.Sync(deployContext, namespace) + namespace.GetAnnotations()[deploy.CheEclipseOrgNamespace] = ctx.CheCluster.Namespace + _, err = deploy.Sync(ctx, namespace) if err != nil { - return false, err + return reconcile.Result{Requeue: true}, false, err } } } // check if DW exists on the cluster devWorkspaceWebHookExistedBeforeSync, err := deploy.Get( - deployContext, + ctx, client.ObjectKey{Name: DevWorkspaceWebhookName}, &admissionregistrationv1.MutatingWebhookConfiguration{}, ) for _, syncItem := range syncItems { - _, err := syncItem(deployContext) + _, err := syncItem(ctx) if err != nil { - return false, err + return reconcile.Result{Requeue: true}, false, err } } @@ -155,88 +157,9 @@ func ReconcileDevWorkspace(deployContext *deploy.DeployContext) (done bool, err } } - return true, nil -} - -func isDevWorkspaceDeploymentExists(deployContext *deploy.DeployContext) (bool, error) { - return deploy.Get(deployContext, types.NamespacedName{ - Namespace: DevWorkspaceNamespace, - Name: DevWorkspaceDeploymentName, - }, &appsv1.Deployment{}) -} - -func isDevWorkspaceOperatorCSVExists(deployContext *deploy.DeployContext) bool { - // If clusterserviceversions resource doesn't exist in cluster DWO as well will not be present - if !util.HasK8SResourceObject(deployContext.ClusterAPI.DiscoveryClient, ClusterServiceVersionResourceName) { - return false - } - - csvList := &operatorsv1alpha1.ClusterServiceVersionList{} - err := deployContext.ClusterAPI.Client.List(context.TODO(), csvList, &client.ListOptions{}) - if err != nil { - return false - } - - for _, csv := range csvList.Items { - if strings.HasPrefix(csv.Name, DevWorkspaceCSVNamePrefix) { - return true - } - } - - return false + return reconcile.Result{}, true, nil } -func doesWebTerminalSubscriptionExist(deployContext *deploy.DeployContext) (bool, error) { - // If subscriptions resource doesn't exist in cluster WTO as well will not be present - if !util.HasK8SResourceObject(deployContext.ClusterAPI.DiscoveryClient, SubscriptionResourceName) { - return false, nil - } - - subscription := &operatorsv1alpha1.Subscription{} - if err := deployContext.ClusterAPI.NonCachingClient.Get( - context.TODO(), - types.NamespacedName{ - Name: WebTerminalOperatorSubscriptionName, - Namespace: WebTerminalOperatorNamespace, - }, - subscription); err != nil { - - if apierrors.IsNotFound(err) { - return false, nil - } - return false, err - } - - return true, nil -} - -func createDwNamespace(deployContext *deploy.DeployContext) (bool, error) { - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: DevWorkspaceNamespace, - Annotations: map[string]string{ - deploy.CheEclipseOrgNamespace: deployContext.CheCluster.Namespace, - }, - }, - Spec: corev1.NamespaceSpec{}, - } - - return deploy.CreateIfNotExists(deployContext, namespace) -} - -func isOnlyOneOperatorManagesDWResources(deployContext *deploy.DeployContext) (bool, error) { - cheClusters := &orgv1.CheClusterList{} - err := deployContext.ClusterAPI.NonCachingClient.List(context.TODO(), cheClusters) - if err != nil { - return false, err - } - - devWorkspaceEnabledNum := 0 - for _, cheCluster := range cheClusters.Items { - if cheCluster.Spec.DevWorkspace.Enable { - devWorkspaceEnabledNum++ - } - } - - return devWorkspaceEnabledNum == 1, nil +func (d *DevWorkspaceReconciler) Finalize(ctx *deploy.DeployContext) error { + return nil } diff --git a/pkg/deploy/dev-workspace/sync.go b/pkg/deploy/dev-workspace/dev_workspace_syncer.go similarity index 100% rename from pkg/deploy/dev-workspace/sync.go rename to pkg/deploy/dev-workspace/dev_workspace_syncer.go diff --git a/pkg/deploy/dev-workspace/sync_test.go b/pkg/deploy/dev-workspace/dev_workspace_syncer_test.go similarity index 100% rename from pkg/deploy/dev-workspace/sync_test.go rename to pkg/deploy/dev-workspace/dev_workspace_syncer_test.go diff --git a/pkg/deploy/dev-workspace/dev_workspace_test.go b/pkg/deploy/dev-workspace/dev_workspace_test.go index ffcd149970..5525e28a7f 100644 --- a/pkg/deploy/dev-workspace/dev_workspace_test.go +++ b/pkg/deploy/dev-workspace/dev_workspace_test.go @@ -138,7 +138,8 @@ func TestReconcileDevWorkspace(t *testing.T) { err := os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "true") assert.NoError(t, err) - done, err := ReconcileDevWorkspace(deployContext) + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) assert.NoError(t, err, "Reconcile failed") assert.True(t, done, "Dev Workspace operator has not been provisioned") }) @@ -165,9 +166,10 @@ func TestShouldNotReconcileDevWorkspaceIfForbidden(t *testing.T) { err := os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "false") assert.NoError(t, err) - reconciled, err := ReconcileDevWorkspace(deployContext) + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) - assert.False(t, reconciled, "DevWorkspace should not be reconciled") + assert.False(t, done, "DevWorkspace should not be reconciled") assert.NotNil(t, err, "Error expected") assert.True(t, strings.Contains(err.Error(), "deploy Eclipse Che from tech-preview channel"), "Unrecognized error occurred %v", err) } @@ -203,10 +205,11 @@ func TestShouldReconcileDevWorkspaceIfDevWorkspaceDeploymentExists(t *testing.T) err := os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "false") assert.NoError(t, err) - reconciled, err := ReconcileDevWorkspace(deployContext) + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) assert.Nil(t, err, "Reconciliation error occurred %v", err) - assert.True(t, reconciled, "DevWorkspace should be reconciled.") + assert.True(t, done, "DevWorkspace should be reconciled.") } func TestReconcileWhenWebTerminalSubscriptionExists(t *testing.T) { @@ -243,9 +246,11 @@ func TestReconcileWhenWebTerminalSubscriptionExists(t *testing.T) { err := os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "true") assert.NoError(t, err) - isDone, err := ReconcileDevWorkspace(deployContext) + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) + assert.NoError(t, err) - assert.True(t, isDone) + assert.True(t, done) // verify that DWO is not provisioned namespace := &corev1.Namespace{} @@ -292,9 +297,10 @@ func TestReconcileDevWorkspaceCheckIfCSVExists(t *testing.T) { err = os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "true") assert.NoError(t, err) - reconciled, _ := ReconcileDevWorkspace(deployContext) + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) - assert.True(t, reconciled, "Reconcile is not triggered") + assert.True(t, done, "Reconcile is not triggered") // Get Devworkspace namespace. If error is thrown means devworkspace is not anymore installed if CSV is detected err = deployContext.ClusterAPI.Client.Get(context.TODO(), client.ObjectKey{Name: DevWorkspaceNamespace}, &corev1.Namespace{}) @@ -327,9 +333,11 @@ func TestReconcileDevWorkspaceIfUnmanagedDWONamespaceExists(t *testing.T) { util.IsOpenShift4 = true err = os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "true") assert.NoError(t, err) - reconciled, _ := ReconcileDevWorkspace(deployContext) - assert.True(t, reconciled, "Reconcile is not triggered") + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) + + assert.True(t, done, "Reconcile is not triggered") // check is reconcile created deployment if existing namespace is not annotated in che specific way err = deployContext.ClusterAPI.Client.Get(context.TODO(), client.ObjectKey{Name: DevWorkspaceDeploymentName}, &appsv1.Deployment{}) @@ -370,9 +378,10 @@ func TestReconcileDevWorkspaceIfManagedDWONamespaceExists(t *testing.T) { err = os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "true") assert.NoError(t, err) - reconciled, err := ReconcileDevWorkspace(deployContext) + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) - assert.True(t, reconciled, "Reconcile is not triggered") + assert.True(t, done, "Reconcile is not triggered") assert.NoError(t, err, "Reconcile failed") // check is reconcile created deployment if existing namespace is not annotated in che specific way @@ -418,9 +427,10 @@ func TestReconcileDevWorkspaceIfManagedDWOShouldBeTakenUnderControl(t *testing.T err = os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "true") assert.NoError(t, err) - reconciled, err := ReconcileDevWorkspace(deployContext) + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) - assert.True(t, reconciled, "Reconcile is not triggered") + assert.True(t, done, "Reconcile is not triggered") assert.NoError(t, err, "Reconcile failed") // check is reconcile updated namespace with according way @@ -487,9 +497,10 @@ func TestReconcileDevWorkspaceIfManagedDWOShouldNotBeTakenUnderControl(t *testin err = os.Setenv("ALLOW_DEVWORKSPACE_ENGINE", "true") assert.NoError(t, err) - reconciled, err := ReconcileDevWorkspace(deployContext) + devWorkspaceReconciler := NewDevWorkspaceReconciler() + _, done, err := devWorkspaceReconciler.Reconcile(deployContext) - assert.True(t, reconciled, "Reconcile is not triggered") + assert.True(t, done, "Reconcile is not triggered") assert.NoError(t, err, "Reconcile failed") // check is reconcile updated namespace with according way diff --git a/pkg/deploy/dev-workspace/dev_workspace_utils.go b/pkg/deploy/dev-workspace/dev_workspace_utils.go new file mode 100644 index 0000000000..b280b4d5f6 --- /dev/null +++ b/pkg/deploy/dev-workspace/dev_workspace_utils.go @@ -0,0 +1,114 @@ +// +// 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 devworkspace + +import ( + "context" + "strings" + + orgv1 "github.com/eclipse-che/che-operator/api/v1" + "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/util" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func isDevWorkspaceDeploymentExists(deployContext *deploy.DeployContext) (bool, error) { + return deploy.Get(deployContext, types.NamespacedName{ + Namespace: DevWorkspaceNamespace, + Name: DevWorkspaceDeploymentName, + }, &appsv1.Deployment{}) +} + +func isDevWorkspaceOperatorCSVExists(deployContext *deploy.DeployContext) bool { + // If clusterserviceversions resource doesn't exist in cluster DWO as well will not be present + if !util.HasK8SResourceObject(deployContext.ClusterAPI.DiscoveryClient, ClusterServiceVersionResourceName) { + return false + } + + csvList := &operatorsv1alpha1.ClusterServiceVersionList{} + err := deployContext.ClusterAPI.Client.List(context.TODO(), csvList, &client.ListOptions{}) + if err != nil { + logrus.Errorf("Failed to list csv: %v", err) + return false + } + + for _, csv := range csvList.Items { + if strings.HasPrefix(csv.Name, DevWorkspaceCSVNamePrefix) { + return true + } + } + + return false +} + +func isWebTerminalSubscriptionExist(deployContext *deploy.DeployContext) (bool, error) { + // If subscriptions resource doesn't exist in cluster WTO as well will not be present + if !util.HasK8SResourceObject(deployContext.ClusterAPI.DiscoveryClient, SubscriptionResourceName) { + return false, nil + } + + subscription := &operatorsv1alpha1.Subscription{} + if err := deployContext.ClusterAPI.NonCachingClient.Get( + context.TODO(), + types.NamespacedName{ + Name: WebTerminalOperatorSubscriptionName, + Namespace: WebTerminalOperatorNamespace, + }, + subscription); err != nil { + + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + return true, nil +} + +func createDwNamespace(deployContext *deploy.DeployContext) (bool, error) { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: DevWorkspaceNamespace, + Annotations: map[string]string{ + deploy.CheEclipseOrgNamespace: deployContext.CheCluster.Namespace, + }, + }, + Spec: corev1.NamespaceSpec{}, + } + + return deploy.CreateIfNotExists(deployContext, namespace) +} + +func isOnlyOneOperatorManagesDWResources(deployContext *deploy.DeployContext) (bool, error) { + cheClusters := &orgv1.CheClusterList{} + err := deployContext.ClusterAPI.NonCachingClient.List(context.TODO(), cheClusters) + if err != nil { + return false, err + } + + devWorkspaceEnabledNum := 0 + for _, cheCluster := range cheClusters.Items { + if cheCluster.Spec.DevWorkspace.Enable { + devWorkspaceEnabledNum++ + } + } + + return devWorkspaceEnabledNum == 1, nil +} diff --git a/pkg/deploy/rbac/gateway_permissions.go b/pkg/deploy/rbac/gateway_permissions.go new file mode 100644 index 0000000000..72f0eeedf0 --- /dev/null +++ b/pkg/deploy/rbac/gateway_permissions.go @@ -0,0 +1,102 @@ +// +// 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 ( + orgv1 "github.com/eclipse-che/che-operator/api/v1" + "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/deploy/gateway" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + CheGatewayClusterPermissionsFinalizerName = "cheGateway.clusterpermissions.finalizers.che.eclipse.org" +) + +type GatewayPermissionsReconciler struct { + deploy.Reconcilable +} + +func NewGatewayPermissionsReconciler() *GatewayPermissionsReconciler { + return &GatewayPermissionsReconciler{} +} + +func (gp *GatewayPermissionsReconciler) Reconcile(ctx *deploy.DeployContext) (reconcile.Result, bool, error) { + if ctx.CheCluster.IsNativeUserModeEnabled() { + name := gp.gatewayPermissionsName(ctx.CheCluster) + if done, err := deploy.SyncClusterRoleToCluster(ctx, name, gp.getGatewayClusterRoleRules()); !done { + return reconcile.Result{Requeue: true}, false, err + } + + if done, err := deploy.SyncClusterRoleBindingToCluster(ctx, name, gateway.GatewayServiceName, name); !done { + return reconcile.Result{Requeue: true}, false, err + } + + if err := deploy.AppendFinalizer(ctx, CheGatewayClusterPermissionsFinalizerName); err != nil { + return reconcile.Result{Requeue: true}, false, err + } + } else { + if done, err := gp.deleteGatewayPermissions(ctx); !done { + return reconcile.Result{Requeue: true}, false, err + } + } + + return reconcile.Result{}, true, nil +} + +func (gp *GatewayPermissionsReconciler) Finalize(ctx *deploy.DeployContext) error { + _, err := gp.deleteGatewayPermissions(ctx) + if err != nil { + return err + } + + return nil +} + +func (gp *GatewayPermissionsReconciler) deleteGatewayPermissions(deployContext *deploy.DeployContext) (bool, error) { + name := gp.gatewayPermissionsName(deployContext.CheCluster) + if done, err := deploy.Delete(deployContext, types.NamespacedName{Name: name}, &rbacv1.ClusterRoleBinding{}); !done { + return false, err + } + + if done, err := deploy.Delete(deployContext, types.NamespacedName{Name: name}, &rbacv1.ClusterRole{}); !done { + return false, err + } + + if err := deploy.DeleteFinalizer(deployContext, CheGatewayClusterPermissionsFinalizerName); err != nil { + return false, err + } + + return true, nil +} + +func (gp *GatewayPermissionsReconciler) gatewayPermissionsName(instance *orgv1.CheCluster) string { + return instance.Namespace + "-" + gateway.GatewayServiceName +} + +func (gp *GatewayPermissionsReconciler) getGatewayClusterRoleRules() []rbacv1.PolicyRule { + return []rbacv1.PolicyRule{ + { + Verbs: []string{"create"}, + APIGroups: []string{"authentication.k8s.io"}, + Resources: []string{"tokenreviews"}, + }, + { + Verbs: []string{"create"}, + APIGroups: []string{"authorization.k8s.io"}, + Resources: []string{"subjectaccessreviews"}, + }, + } +} diff --git a/pkg/deploy/rbac/init_test.go b/pkg/deploy/rbac/init_test.go new file mode 100644 index 0000000000..c57f210ba5 --- /dev/null +++ b/pkg/deploy/rbac/init_test.go @@ -0,0 +1,21 @@ +// +// Copyright (c) 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 "github.com/eclipse-che/che-operator/pkg/deploy" + +func init() { + err := deploy.InitTestDefaultsFromDeployment("../../../config/manager/manager.yaml") + if err != nil { + panic(err) + } +} diff --git a/pkg/deploy/rbac/rbac.go b/pkg/deploy/rbac/rbac.go new file mode 100644 index 0000000000..4cd1a0db94 --- /dev/null +++ b/pkg/deploy/rbac/rbac.go @@ -0,0 +1,74 @@ +// +// 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/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 *deploy.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, deploy.CheServiceAccountName) + if !done { + return reconcile.Result{Requeue: true}, false, err + } + + if len(ctx.CheCluster.Spec.Server.CheClusterRoles) > 0 { + cheClusterRoles := strings.Split(ctx.CheCluster.Spec.Server.CheClusterRoles, ",") + for _, cheClusterRole := range cheClusterRoles { + cheClusterRole := strings.TrimSpace(cheClusterRole) + cheClusterRoleBindingName := cheClusterRole + done, err := deploy.SyncClusterRoleBindingAndAddFinalizerToCluster(ctx, cheClusterRoleBindingName, deploy.CheServiceAccountName, cheClusterRole) + if !done { + return reconcile.Result{Requeue: true}, false, err + } + } + } + + return reconcile.Result{}, true, err +} + +func (c *CheServerPermissionsReconciler) Finalize(ctx *deploy.DeployContext) error { + if len(ctx.CheCluster.Spec.Server.CheClusterRoles) > 0 { + cheClusterRoles := strings.Split(ctx.CheCluster.Spec.Server.CheClusterRoles, ",") + for _, cheClusterRole := range cheClusterRoles { + cheClusterRole := strings.TrimSpace(cheClusterRole) + cheClusterRoleBindingName := cheClusterRole + if err := deploy.ReconcileClusterRoleBindingFinalizer(ctx, cheClusterRoleBindingName); err != nil { + logrus.Error(err) + } + + // Removes any legacy CRB https://github.com/eclipse/che/issues/19506 + cheClusterRoleBindingName = deploy.GetLegacyUniqueClusterRoleBindingName(ctx, deploy.CheServiceAccountName, cheClusterRole) + if err := deploy.ReconcileLegacyClusterRoleBindingFinalizer(ctx, cheClusterRoleBindingName); err != nil { + logrus.Error(err) + } + } + } + + return nil +} diff --git a/controllers/che/workspace_namespace_permission.go b/pkg/deploy/rbac/workspace_permissions.go similarity index 74% rename from controllers/che/workspace_namespace_permission.go rename to pkg/deploy/rbac/workspace_permissions.go index 3196517a7e..1dfec791dc 100644 --- a/controllers/che/workspace_namespace_permission.go +++ b/pkg/deploy/rbac/workspace_permissions.go @@ -10,15 +10,16 @@ // Red Hat, Inc. - initial API and implementation // -package che +package rbac import ( "fmt" "github.com/eclipse-che/che-operator/pkg/deploy" "github.com/eclipse-che/che-operator/pkg/util" - rbac "k8s.io/api/rbac/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const ( @@ -45,24 +46,60 @@ const ( DevWorkspacePermissionsFinalizerName = "devWorkspace.permissions.finalizers.che.eclipse.org" ) -// Reconcile workspace permissions based on workspace strategy -func (r *CheClusterReconciler) reconcileWorkspacePermissions(deployContext *deploy.DeployContext) (bool, error) { - done, err := r.delegateWorkspacePermissionsInTheDifferNamespaceThanChe(deployContext) +type WorkspacePermissionsReconciler struct { + deploy.Reconcilable +} + +func NewWorkspacePermissionsReconciler() *WorkspacePermissionsReconciler { + return &WorkspacePermissionsReconciler{} +} + +func (wp *WorkspacePermissionsReconciler) Reconcile(ctx *deploy.DeployContext) (reconcile.Result, bool, error) { + done, err := wp.delegateWorkspacePermissionsInTheDifferNamespaceThanChe(ctx) if !done { - return false, err + return reconcile.Result{Requeue: true}, false, err } - done, err = r.delegateNamespaceEditorPermissions(deployContext) + done, err = wp.delegateNamespaceEditorPermissions(ctx) if !done { - return false, err + return reconcile.Result{Requeue: true}, false, err } - done, err = r.delegateDevWorkspacePermissions(deployContext) + done, err = wp.delegateDevWorkspacePermissions(ctx) if !done { - return false, err + return reconcile.Result{Requeue: true}, false, err } - return true, nil + // If the user specified an additional cluster role to use for the Che workspace, create a role binding for it + // Use a role binding instead of a cluster role binding to keep the additional access scoped to the workspace's namespace + workspaceClusterRole := ctx.CheCluster.Spec.Server.CheWorkspaceClusterRole + if workspaceClusterRole != "" { + done, err := deploy.SyncRoleBindingToCluster(ctx, "che-workspace-custom", "view", workspaceClusterRole, "ClusterRole") + if !done { + return reconcile.Result{Requeue: true}, false, err + } + } + + return reconcile.Result{}, true, nil +} + +func (wp *WorkspacePermissionsReconciler) Finalize(ctx *deploy.DeployContext) error { + _, err := wp.removeNamespaceEditorPermissions(ctx) + if err != nil { + return err + } + + _, err = wp.removeDevWorkspacePermissions(ctx) + if err != nil { + return err + } + + _, err = wp.removeWorkspacePermissionsInTheDifferNamespaceThanChe(ctx) + if err != nil { + return err + } + + return nil } // Create cluster roles and cluster role bindings for "che" service account. @@ -74,12 +111,12 @@ func (r *CheClusterReconciler) reconcileWorkspacePermissions(deployContext *depl // 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 (r *CheClusterReconciler) delegateWorkspacePermissionsInTheDifferNamespaceThanChe(deployContext *deploy.DeployContext) (bool, error) { +func (wp *WorkspacePermissionsReconciler) delegateWorkspacePermissionsInTheDifferNamespaceThanChe(deployContext *deploy.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, getWorkspacesPolicies()) + done, err := deploy.SyncClusterRoleToCluster(deployContext, сheWorkspacesClusterRoleName, wp.getWorkspacesPolicies()) if !done { return false, err } @@ -93,16 +130,16 @@ func (r *CheClusterReconciler) delegateWorkspacePermissionsInTheDifferNamespaceT return err == nil, err } -func (r *CheClusterReconciler) removeWorkspacePermissionsInTheDifferNamespaceThanChe(deployContext *deploy.DeployContext) (bool, error) { +func (wp *WorkspacePermissionsReconciler) removeWorkspacePermissionsInTheDifferNamespaceThanChe(deployContext *deploy.DeployContext) (bool, error) { cheWorkspacesClusterRoleName := fmt.Sprintf(CheWorkspacesClusterRoleNameTemplate, deployContext.CheCluster.Namespace) cheWorkspacesClusterRoleBindingName := cheWorkspacesClusterRoleName - done, err := deploy.Delete(deployContext, types.NamespacedName{Name: cheWorkspacesClusterRoleName}, &rbac.ClusterRole{}) + done, err := deploy.Delete(deployContext, types.NamespacedName{Name: cheWorkspacesClusterRoleName}, &rbacv1.ClusterRole{}) if !done { return false, err } - done, err = deploy.Delete(deployContext, types.NamespacedName{Name: cheWorkspacesClusterRoleBindingName}, &rbac.ClusterRoleBinding{}) + done, err = deploy.Delete(deployContext, types.NamespacedName{Name: cheWorkspacesClusterRoleBindingName}, &rbacv1.ClusterRoleBinding{}) if !done { return false, err } @@ -111,12 +148,12 @@ func (r *CheClusterReconciler) removeWorkspacePermissionsInTheDifferNamespaceTha return err == nil, err } -func (r *CheClusterReconciler) delegateNamespaceEditorPermissions(deployContext *deploy.DeployContext) (bool, error) { +func (wp *WorkspacePermissionsReconciler) delegateNamespaceEditorPermissions(deployContext *deploy.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, getNamespaceEditorPolicies()) + done, err := deploy.SyncClusterRoleToCluster(deployContext, сheNamespaceEditorClusterRoleName, wp.getNamespaceEditorPolicies()) if !done { return false, err } @@ -130,15 +167,15 @@ func (r *CheClusterReconciler) delegateNamespaceEditorPermissions(deployContext return err == nil, err } -func (r *CheClusterReconciler) removeNamespaceEditorPermissions(deployContext *deploy.DeployContext) (bool, error) { +func (wp *WorkspacePermissionsReconciler) removeNamespaceEditorPermissions(deployContext *deploy.DeployContext) (bool, error) { cheNamespaceEditorClusterRoleName := fmt.Sprintf(CheNamespaceEditorClusterRoleNameTemplate, deployContext.CheCluster.Namespace) - done, err := deploy.Delete(deployContext, types.NamespacedName{Name: cheNamespaceEditorClusterRoleName}, &rbac.ClusterRole{}) + done, err := deploy.Delete(deployContext, types.NamespacedName{Name: cheNamespaceEditorClusterRoleName}, &rbacv1.ClusterRole{}) if !done { return false, err } - done, err = deploy.Delete(deployContext, types.NamespacedName{Name: cheNamespaceEditorClusterRoleName}, &rbac.ClusterRoleBinding{}) + done, err = deploy.Delete(deployContext, types.NamespacedName{Name: cheNamespaceEditorClusterRoleName}, &rbacv1.ClusterRoleBinding{}) if !done { return false, err } @@ -147,11 +184,11 @@ func (r *CheClusterReconciler) removeNamespaceEditorPermissions(deployContext *d return err == nil, err } -func (r *CheClusterReconciler) delegateDevWorkspacePermissions(deployContext *deploy.DeployContext) (bool, error) { +func (wp *WorkspacePermissionsReconciler) delegateDevWorkspacePermissions(deployContext *deploy.DeployContext) (bool, error) { devWorkspaceClusterRoleName := fmt.Sprintf(DevWorkspaceClusterRoleNameTemplate, deployContext.CheCluster.Namespace) devWorkspaceClusterRoleBindingName := devWorkspaceClusterRoleName - done, err := deploy.SyncClusterRoleToCluster(deployContext, devWorkspaceClusterRoleName, getDevWorkspacePolicies()) + done, err := deploy.SyncClusterRoleToCluster(deployContext, devWorkspaceClusterRoleName, wp.getDevWorkspacePolicies()) if !done { return false, err } @@ -165,16 +202,16 @@ func (r *CheClusterReconciler) delegateDevWorkspacePermissions(deployContext *de return err == nil, err } -func (r *CheClusterReconciler) removeDevWorkspacePermissions(deployContext *deploy.DeployContext) (bool, error) { +func (wp *WorkspacePermissionsReconciler) removeDevWorkspacePermissions(deployContext *deploy.DeployContext) (bool, error) { devWorkspaceClusterRoleName := fmt.Sprintf(DevWorkspaceClusterRoleNameTemplate, deployContext.CheCluster.Namespace) devWorkspaceClusterRoleBindingName := devWorkspaceClusterRoleName - done, err := deploy.Delete(deployContext, types.NamespacedName{Name: devWorkspaceClusterRoleName}, &rbac.ClusterRole{}) + done, err := deploy.Delete(deployContext, types.NamespacedName{Name: devWorkspaceClusterRoleName}, &rbacv1.ClusterRole{}) if !done { return false, err } - done, err = deploy.Delete(deployContext, types.NamespacedName{Name: devWorkspaceClusterRoleBindingName}, &rbac.ClusterRoleBinding{}) + done, err = deploy.Delete(deployContext, types.NamespacedName{Name: devWorkspaceClusterRoleBindingName}, &rbacv1.ClusterRoleBinding{}) if !done { return false, err } @@ -183,26 +220,8 @@ func (r *CheClusterReconciler) removeDevWorkspacePermissions(deployContext *depl return err == nil, err } -func (r *CheClusterReconciler) reconcileWorkspacePermissionsFinalizers(deployContext *deploy.DeployContext) (bool, error) { - if !deployContext.CheCluster.ObjectMeta.DeletionTimestamp.IsZero() { - done, err := r.removeNamespaceEditorPermissions(deployContext) - if !done { - return false, err - } - - done, err = r.removeDevWorkspacePermissions(deployContext) - if !done { - return false, err - } - - return r.removeWorkspacePermissionsInTheDifferNamespaceThanChe(deployContext) - } - - return true, nil -} - -func getDevWorkspacePolicies() []rbac.PolicyRule { - k8sPolicies := []rbac.PolicyRule{ +func (wp *WorkspacePermissionsReconciler) getDevWorkspacePolicies() []rbacv1.PolicyRule { + k8sPolicies := []rbacv1.PolicyRule{ { APIGroups: []string{"workspace.devfile.io"}, Resources: []string{"devworkspaces", "devworkspacetemplates"}, @@ -213,8 +232,8 @@ func getDevWorkspacePolicies() []rbac.PolicyRule { return k8sPolicies } -func getNamespaceEditorPolicies() []rbac.PolicyRule { - k8sPolicies := []rbac.PolicyRule{ +func (wp *WorkspacePermissionsReconciler) getNamespaceEditorPolicies() []rbacv1.PolicyRule { + k8sPolicies := []rbacv1.PolicyRule{ { APIGroups: []string{""}, Resources: []string{"namespaces"}, @@ -222,7 +241,7 @@ func getNamespaceEditorPolicies() []rbac.PolicyRule { }, } - openshiftPolicies := []rbac.PolicyRule{ + openshiftPolicies := []rbacv1.PolicyRule{ { APIGroups: []string{"project.openshift.io"}, Resources: []string{"projectrequests"}, @@ -241,8 +260,8 @@ func getNamespaceEditorPolicies() []rbac.PolicyRule { return k8sPolicies } -func getWorkspacesPolicies() []rbac.PolicyRule { - k8sPolicies := []rbac.PolicyRule{ +func (c *WorkspacePermissionsReconciler) getWorkspacesPolicies() []rbacv1.PolicyRule { + k8sPolicies := []rbacv1.PolicyRule{ { APIGroups: []string{""}, Resources: []string{"serviceaccounts"}, @@ -334,7 +353,7 @@ func getWorkspacesPolicies() []rbac.PolicyRule { Verbs: []string{"get", "list", "watch"}, }, } - openshiftPolicies := []rbac.PolicyRule{ + openshiftPolicies := []rbacv1.PolicyRule{ { APIGroups: []string{"route.openshift.io"}, Resources: []string{"routes"}, diff --git a/pkg/deploy/rbac/workspace_permissions_test.go b/pkg/deploy/rbac/workspace_permissions_test.go new file mode 100644 index 0000000000..2cf7aa274a --- /dev/null +++ b/pkg/deploy/rbac/workspace_permissions_test.go @@ -0,0 +1,104 @@ +// +// 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" + + orgv1 "github.com/eclipse-che/che-operator/api/v1" + "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/util" + "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" + "k8s.io/utils/pointer" +) + +func TestReconcileWorkspacePermissions(t *testing.T) { + util.IsOpenShift = true + + type testCase struct { + name string + initObjects []runtime.Object + checluster *orgv1.CheCluster + } + + testCases := []testCase{ + { + name: "che-operator should delegate permission for workspaces in differ namespace than Che. WorkspaceNamespaceDefault = 'some-test-namespace'", + initObjects: []runtime.Object{}, + checluster: &orgv1.CheCluster{ + ObjectMeta: v1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + Spec: orgv1.CheClusterSpec{ + Server: orgv1.CheClusterSpecServer{ + WorkspaceNamespaceDefault: "some-test-namespace", + }, + Auth: orgv1.CheClusterSpecAuth{ + OpenShiftoAuth: pointer.BoolPtr(false), + }, + }, + }, + }, + { + 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: &orgv1.CheCluster{ + ObjectMeta: v1.ObjectMeta{ + Name: "eclipse-che", + Namespace: "eclipse-che", + }, + Spec: orgv1.CheClusterSpec{ + Server: orgv1.CheClusterSpecServer{ + CustomCheProperties: map[string]string{ + "CHE_INFRA_KUBERNETES_NAMESPACE_DEFAULT": "some-test-namespace", + }, + }, + Auth: orgv1.CheClusterSpecAuth{ + OpenShiftoAuth: pointer.BoolPtr(false), + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ctx := deploy.GetTestDeployContext(testCase.checluster, testCase.initObjects) + + wp := NewWorkspacePermissionsReconciler() + _, done, err := wp.Reconcile(ctx) + + assert.Nil(t, err) + assert.True(t, done) + assert.True(t, util.ContainsString(ctx.CheCluster.Finalizers, CheWorkspacesClusterPermissionsFinalizerName)) + assert.True(t, util.ContainsString(ctx.CheCluster.Finalizers, NamespacesEditorPermissionsFinalizerName)) + assert.True(t, util.ContainsString(ctx.CheCluster.Finalizers, DevWorkspacePermissionsFinalizerName)) + + name := "eclipse-che-cheworkspaces-clusterrole" + assert.True(t, util.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRole{})) + assert.True(t, util.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{})) + + name = "eclipse-che-cheworkspaces-namespaces-clusterrole" + assert.True(t, util.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRole{})) + assert.True(t, util.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{})) + + name = "eclipse-che-cheworkspaces-devworkspace-clusterrole" + assert.True(t, util.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRole{})) + assert.True(t, util.IsObjectExists(ctx.ClusterAPI.Client, types.NamespacedName{Name: name}, &rbac.ClusterRoleBinding{})) + }) + } +} diff --git a/pkg/util/test_util.go b/pkg/util/test_util.go index 1d63b7b4c9..ea58d71ffa 100644 --- a/pkg/util/test_util.go +++ b/pkg/util/test_util.go @@ -12,11 +12,14 @@ package util import ( + "context" "testing" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) type TestExpectedResources struct { @@ -102,3 +105,12 @@ func FindVolumeMount(volumes []corev1.VolumeMount, name string) corev1.VolumeMou return corev1.VolumeMount{} } + +func IsObjectExists(client client.Client, key types.NamespacedName, blueprint client.Object) bool { + err := client.Get(context.TODO(), key, blueprint) + if err != nil { + return false + } + + return true +}