From 5bb3d37a68c3343ec45a0c75e9eab967ac2ce32f Mon Sep 17 00:00:00 2001 From: Angel Misevski Date: Wed, 19 Oct 2022 11:27:05 -0600 Subject: [PATCH] Add tests for role/rolebinding provisioning Signed-off-by: Angel Misevski --- pkg/provision/workspace/rbac/common_test.go | 222 +++++++++++++++++ pkg/provision/workspace/rbac/finalize_test.go | 228 ++++++++++++++++++ pkg/provision/workspace/rbac/migrate_test.go | 130 ++++++++++ pkg/provision/workspace/rbac/role_test.go | 113 +++++++++ .../workspace/rbac/rolebinding_test.go | 154 ++++++++++++ 5 files changed, 847 insertions(+) create mode 100644 pkg/provision/workspace/rbac/common_test.go create mode 100644 pkg/provision/workspace/rbac/finalize_test.go create mode 100644 pkg/provision/workspace/rbac/migrate_test.go create mode 100644 pkg/provision/workspace/rbac/role_test.go create mode 100644 pkg/provision/workspace/rbac/rolebinding_test.go diff --git a/pkg/provision/workspace/rbac/common_test.go b/pkg/provision/workspace/rbac/common_test.go new file mode 100644 index 000000000..3b3fb7242 --- /dev/null +++ b/pkg/provision/workspace/rbac/common_test.go @@ -0,0 +1,222 @@ +// Copyright (c) 2019-2022 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rbac + +import ( + "context" + "fmt" + "testing" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + "github.com/devfile/devworkspace-operator/pkg/common" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/pkg/provision/sync" + testlog "github.com/go-logr/logr/testing" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + testNamespace = "test-namespace" + testSCCName = "test-scc" +) + +var ( + scheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(dw.AddToScheme(scheme)) +} + +var ( + oldRole = &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.OldWorkspaceRoleName(), + Namespace: testNamespace, + }, + Rules: []rbacv1.PolicyRule{}, + } + oldRolebinding = &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.OldWorkspaceRolebindingName(), + Namespace: testNamespace, + }, + } + newRole = &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.WorkspaceRoleName(), + Namespace: testNamespace, + }, + Rules: []rbacv1.PolicyRule{}, + } + newRolebinding = &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.WorkspaceRolebindingName(), + Namespace: testNamespace, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: common.WorkspaceRoleName(), + }, + } + newSCCRole = &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.WorkspaceSCCRoleName(testSCCName), + Namespace: testNamespace, + }, + Rules: []rbacv1.PolicyRule{}, + } + newSCCRolebinding = &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.WorkspaceSCCRolebindingName(testSCCName), + Namespace: testNamespace, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: common.WorkspaceSCCRoleName(testSCCName), + }, + } +) + +func TestSyncRBAC(t *testing.T) { + testdw1 := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + testdw2 := getTestDevWorkspaceWithAttributes(t, "test-devworkspace2", constants.WorkspaceSCCAttribute, testSCCName) + testdw1SAName := common.ServiceAccountName(testdw1.Status.DevWorkspaceId) + testdw2SAName := common.ServiceAccountName(testdw2.Status.DevWorkspaceId) + api := getTestClusterAPI(t, testdw1.DevWorkspace, testdw2.DevWorkspace, oldRole, oldRolebinding) + // Keep calling SyncRBAC until error returned is nil, to account for multiple steps + iterCount := 0 + maxIters := 30 + retryErr := &RetryError{} + for err := SyncRBAC(testdw1, api); err != nil; err = SyncRBAC(testdw1, api) { + iterCount += 1 + if err == nil { + break + } + if !assert.ErrorAs(t, err, &retryErr, "Unexpected error from SyncRBAC: %s", err) { + return + } + if !assert.LessOrEqual(t, iterCount, maxIters, fmt.Sprintf("SyncRBAC did not sync everything within %d iterations", maxIters)) { + return + } + } + for err := SyncRBAC(testdw2, api); err != nil; err = SyncRBAC(testdw2, api) { + iterCount += 1 + if err == nil { + break + } + if !assert.ErrorAs(t, err, &retryErr, "Unexpected error from SyncRBAC: %s", err) { + return + } + if !assert.LessOrEqual(t, iterCount, maxIters, fmt.Sprintf("SyncRBAC did not sync everything within %d iterations", maxIters)) { + return + } + } + actualRole := &rbacv1.Role{} + err := api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceRoleName(), + Namespace: testNamespace, + }, actualRole) + assert.NoError(t, err, "Role should be created") + + actualSCCRole := &rbacv1.Role{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceSCCRoleName(testSCCName), + Namespace: testNamespace, + }, actualSCCRole) + assert.NoError(t, err, "SCC Role should be created") + + actualRolebinding := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceRolebindingName(), + Namespace: testNamespace, + }, actualRolebinding) + assert.NoError(t, err, "Role should be created") + assert.True(t, testHasSubject(testdw1SAName, testNamespace, actualRolebinding), "Should have testdw1 SA as subject") + assert.True(t, testHasSubject(testdw2SAName, testNamespace, actualRolebinding), "Should have testdw2 SA as subject") + + actualSCCRolebinding := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceSCCRolebindingName(testSCCName), + Namespace: testNamespace, + }, actualSCCRolebinding) + assert.NoError(t, err, "SCC Rolebindind should be created") + assert.True(t, testHasSubject(testdw1SAName, testNamespace, actualSCCRolebinding), "Should have testdw1 SA as subject") + assert.True(t, testHasSubject(testdw2SAName, testNamespace, actualSCCRolebinding), "Should have testdw2 SA as subject") +} + +func getTestClusterAPI(t *testing.T, initialObjects ...client.Object) sync.ClusterAPI { + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initialObjects...).Build() + return sync.ClusterAPI{ + Ctx: context.Background(), + Client: fakeClient, + Scheme: scheme, + Logger: testlog.TestLogger{T: t}, + } +} + +func getTestDevWorkspace(id string) *common.DevWorkspaceWithConfig { + return &common.DevWorkspaceWithConfig{ + DevWorkspace: &dw.DevWorkspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: id, + Namespace: testNamespace, + }, + + Status: dw.DevWorkspaceStatus{ + DevWorkspaceId: id, + }, + }, + } +} + +func getTestDevWorkspaceWithAttributes(t *testing.T, id string, keysAndValues ...string) *common.DevWorkspaceWithConfig { + attr := attributes.Attributes{} + if len(keysAndValues)%2 != 0 { + t.Fatalf("Invalid keysAndValues for getTestDevWorkspaceWithAttributes") + } + for i := 0; i < len(keysAndValues); i += 2 { + attr.PutString(keysAndValues[i], keysAndValues[i+1]) + } + return &common.DevWorkspaceWithConfig{ + DevWorkspace: &dw.DevWorkspace{ + ObjectMeta: metav1.ObjectMeta{ + Name: id, + Namespace: testNamespace, + }, + Spec: dw.DevWorkspaceSpec{ + Template: dw.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: dw.DevWorkspaceTemplateSpecContent{ + Attributes: attr, + }, + }, + }, + Status: dw.DevWorkspaceStatus{ + DevWorkspaceId: id, + }, + }, + } +} diff --git a/pkg/provision/workspace/rbac/finalize_test.go b/pkg/provision/workspace/rbac/finalize_test.go new file mode 100644 index 000000000..546250325 --- /dev/null +++ b/pkg/provision/workspace/rbac/finalize_test.go @@ -0,0 +1,228 @@ +// Copyright (c) 2019-2022 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rbac + +import ( + "fmt" + "testing" + "time" + + "github.com/devfile/devworkspace-operator/pkg/common" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/pkg/infrastructure" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestDeletesRoleAndRolebindingWhenLastWorkspaceIsDeleted(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspace("test-devworkspace") + testdw.DeletionTimestamp = &metav1.Time{Time: time.Now()} + api := getTestClusterAPI(t, testdw.DevWorkspace, newRole, newRolebinding) + retryErr := &RetryError{} + err := FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate role deleted") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + assert.Regexp(t, fmt.Sprintf("deleted role .* in namespace %s", testNamespace), err.Error()) + } + err = FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate rolebinding deleted") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + assert.Regexp(t, fmt.Sprintf("deleted rolebinding .* in namespace %s", testNamespace), err.Error()) + } + err = FinalizeRBAC(testdw, api) + assert.NoError(t, err, "Should not return error once role and rolebinding deleted") +} + +func TestDeletesRoleAndRolebindingWhenAllWorkspacesAreDeleted(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspace("test-devworkspace") + testdw2 := getTestDevWorkspace("test-devworkspace2") + testdw.DeletionTimestamp = &metav1.Time{Time: time.Now()} + testdw2.DeletionTimestamp = &metav1.Time{Time: time.Now()} + api := getTestClusterAPI(t, testdw.DevWorkspace, testdw2.DevWorkspace, newRole, newRolebinding) + + retryErr := &RetryError{} + err := FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate role deleted") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + assert.Regexp(t, fmt.Sprintf("deleted role .* in namespace %s", testNamespace), err.Error()) + } + err = FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate rolebinding deleted") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + assert.Regexp(t, fmt.Sprintf("deleted rolebinding .* in namespace %s", testNamespace), err.Error()) + } + err = FinalizeRBAC(testdw, api) + assert.NoError(t, err, "Should not return error once role and rolebinding deleted") +} + +func TestShouldRemoveWorkspaceSAFromRolebindingWhenDeleted(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspace("test-devworkspace") + testdw2 := getTestDevWorkspace("test-devworkspace2") + testdw.DeletionTimestamp = &metav1.Time{Time: time.Now()} + testdwSAName := common.ServiceAccountName(testdw.Status.DevWorkspaceId) + testdw2SAName := common.ServiceAccountName(testdw2.Status.DevWorkspaceId) + testrb := newRolebinding.DeepCopy() + testrb.Subjects = append(testrb.Subjects, + rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: testdwSAName, + Namespace: testNamespace, + }, rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: testdw2SAName, + Namespace: testNamespace, + }) + api := getTestClusterAPI(t, testdw.DevWorkspace, testdw2.DevWorkspace, testrb, newRole) + retryErr := &RetryError{} + err := FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate rolebinding updated") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + } + err = FinalizeRBAC(testdw, api) + assert.NoError(t, err, "Should not return error once rolebinding is in sync") + + actualRolebinding := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceRolebindingName(), + Namespace: testNamespace, + }, actualRolebinding) + assert.NoError(t, err, "Unexpected test error getting rolebinding") + assert.False(t, testHasSubject(testdwSAName, testNamespace, actualRolebinding), "Should remove delete workspace SA from rolebinding subjects") + assert.True(t, testHasSubject(testdw2SAName, testNamespace, actualRolebinding), "Should leave workspace SA in rolebinding subjects") +} + +func TestFinalizeDoesNothingWhenRolebindingDoesNotExist(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspace("test-devworkspace") + testdw2 := getTestDevWorkspace("test-devworkspace2") + testdw.DeletionTimestamp = &metav1.Time{Time: time.Now()} + api := getTestClusterAPI(t, testdw.DevWorkspace, testdw2.DevWorkspace, newRole) + err := FinalizeRBAC(testdw, api) + assert.NoError(t, err, "Should not return error once rolebinding is in sync") + + actualRolebinding := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceRolebindingName(), + Namespace: testNamespace, + }, actualRolebinding) + if assert.Error(t, err, "Expect error when getting non-existent rolebinding") { + assert.True(t, k8sErrors.IsNotFound(err), "Error should have IsNotFound type") + } +} + +func TestDeletesSCCRoleAndRolebindingWhenLastWorkspaceIsDeleted(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + testdw2 := getTestDevWorkspace("test-devworkspace2") + testdw.DeletionTimestamp = &metav1.Time{Time: time.Now()} + api := getTestClusterAPI(t, testdw.DevWorkspace, testdw2.DevWorkspace, newSCCRole, newSCCRolebinding) + retryErr := &RetryError{} + err := FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate role deleted") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + assert.Regexp(t, fmt.Sprintf("deleted role .* in namespace %s", testNamespace), err.Error()) + } + err = FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate rolebinding deleted") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + assert.Regexp(t, fmt.Sprintf("deleted rolebinding .* in namespace %s", testNamespace), err.Error()) + } + err = FinalizeRBAC(testdw, api) + assert.NoError(t, err, "Should not return error once role and rolebinding deleted") +} + +func TestDeletesSCCRoleAndRolebindingWhenAllWorkspacesAreDeleted(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + testdw2 := getTestDevWorkspaceWithAttributes(t, "test-devworkspace2", constants.WorkspaceSCCAttribute, testSCCName) + testdw.DeletionTimestamp = &metav1.Time{Time: time.Now()} + testdw2.DeletionTimestamp = &metav1.Time{Time: time.Now()} + api := getTestClusterAPI(t, testdw.DevWorkspace, testdw2.DevWorkspace, newSCCRole, newSCCRolebinding) + + retryErr := &RetryError{} + err := FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate role deleted") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + assert.Regexp(t, fmt.Sprintf("deleted role .* in namespace %s", testNamespace), err.Error()) + } + err = FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate rolebinding deleted") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + assert.Regexp(t, fmt.Sprintf("deleted rolebinding .* in namespace %s", testNamespace), err.Error()) + } + err = FinalizeRBAC(testdw, api) + assert.NoError(t, err, "Should not return error once role and rolebinding deleted") +} + +func TestShouldRemoveWorkspaceSAFromSCCRolebindingWhenDeleted(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + testdw2 := getTestDevWorkspaceWithAttributes(t, "test-devworkspace2", constants.WorkspaceSCCAttribute, testSCCName) + testdw.DeletionTimestamp = &metav1.Time{Time: time.Now()} + testdwSAName := common.ServiceAccountName(testdw.Status.DevWorkspaceId) + testdw2SAName := common.ServiceAccountName(testdw2.Status.DevWorkspaceId) + testrb := newSCCRolebinding.DeepCopy() + testrb.Subjects = append(testrb.Subjects, + rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: testdwSAName, + Namespace: testNamespace, + }, rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: testdw2SAName, + Namespace: testNamespace, + }) + api := getTestClusterAPI(t, testdw.DevWorkspace, testdw2.DevWorkspace, testrb, newSCCRole) + retryErr := &RetryError{} + err := FinalizeRBAC(testdw, api) + if assert.Error(t, err, "Should return error to indicate rolebinding updated") { + assert.ErrorAs(t, err, &retryErr, "Error should be RetryError") + } + err = FinalizeRBAC(testdw, api) + assert.NoError(t, err, "Should not return error once rolebinding is in sync") + + actualRolebinding := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceSCCRolebindingName(testSCCName), + Namespace: testNamespace, + }, actualRolebinding) + assert.NoError(t, err, "Unexpected test error getting rolebinding") + assert.False(t, testHasSubject(testdwSAName, testNamespace, actualRolebinding), "Should remove delete workspace SA from rolebinding subjects") + assert.True(t, testHasSubject(testdw2SAName, testNamespace, actualRolebinding), "Should leave workspace SA in rolebinding subjects") +} + +func TestFinalizeDoesNothingWhenSCCRolebindingDoesNotExist(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + testdw2 := getTestDevWorkspaceWithAttributes(t, "test-devworkspace2", constants.WorkspaceSCCAttribute, testSCCName) + testdw.DeletionTimestamp = &metav1.Time{Time: time.Now()} + api := getTestClusterAPI(t, testdw.DevWorkspace, testdw2.DevWorkspace, newSCCRole) + err := FinalizeRBAC(testdw, api) + assert.NoError(t, err, "Should not return error once rolebinding is in sync") + + actualRolebinding := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceSCCRolebindingName(testSCCName), + Namespace: testNamespace, + }, actualRolebinding) + if assert.Error(t, err, "Expect error when getting non-existent rolebinding") { + assert.True(t, k8sErrors.IsNotFound(err), "Error should have IsNotFound type") + } +} diff --git a/pkg/provision/workspace/rbac/migrate_test.go b/pkg/provision/workspace/rbac/migrate_test.go new file mode 100644 index 000000000..de4e724ba --- /dev/null +++ b/pkg/provision/workspace/rbac/migrate_test.go @@ -0,0 +1,130 @@ +// Copyright (c) 2019-2022 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rbac + +import ( + "testing" + + "github.com/devfile/devworkspace-operator/pkg/infrastructure" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" +) + +func TestRemovesOldRBACWhenNewRBACNotPresent(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + api := getTestClusterAPI(t, oldRole, oldRolebinding) + // Expect three calls to be required: 1. delete role, 2. delete rolebinding, 3. return nil + err := cleanupDeprecatedRBAC(testNamespace, api) + retryErr := &RetryError{} + if assert.ErrorAs(t, err, &retryErr, "Error should be of type RetryErr") { + assert.Contains(t, err.Error(), "deleted deprecated DevWorkspace Role") + } + err = cleanupDeprecatedRBAC(testNamespace, api) + if assert.ErrorAs(t, err, &retryErr, "Error should be of type RetryErr") { + assert.Contains(t, err.Error(), "deleted deprecated DevWorkspace RoleBinding") + } + err = cleanupDeprecatedRBAC(testNamespace, api) + assert.NoError(t, err, "Should not return error if old rbac does not exist") + + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: oldRole.Name, + Namespace: testNamespace, + }, &rbacv1.Role{}) + if assert.Error(t, err, "Expect get old role to return IsNotFound error") { + assert.True(t, k8sErrors.IsNotFound(err), "Expect error to have IsNotFound type") + } + + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: oldRolebinding.Name, + Namespace: testNamespace, + }, &rbacv1.RoleBinding{}) + if assert.Error(t, err, "Expect get old role to return IsNotFound error") { + assert.True(t, k8sErrors.IsNotFound(err), "Expect error to have IsNotFound type") + } +} + +func TestDoesNotRemoveOldRBACWhenNewRBACPresent(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + api := getTestClusterAPI(t, oldRole, oldRolebinding, newRole, newRolebinding) + err := cleanupDeprecatedRBAC(testNamespace, api) + assert.NoError(t, err, "Should do nothing if new RBAC role/rolebinding are present") + + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: oldRole.Name, + Namespace: testNamespace, + }, &rbacv1.Role{}) + assert.NoError(t, err, "Old role should still exist if present initially") + + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: oldRolebinding.Name, + Namespace: testNamespace, + }, &rbacv1.RoleBinding{}) + assert.NoError(t, err, "Old rolebinding should still exist if present initially") +} + +func TestRemovesOldRolebindingWhenNewRolebindingNotPresent(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + api := getTestClusterAPI(t, oldRole, oldRolebinding, newRole) + // Expect two calls to be required: 1. delete rolebinding, 2. return nil + err := cleanupDeprecatedRBAC(testNamespace, api) + retryErr := &RetryError{} + if assert.ErrorAs(t, err, &retryErr, "Error should be of type RetryErr") { + assert.Contains(t, err.Error(), "deleted deprecated DevWorkspace RoleBinding") + } + err = cleanupDeprecatedRBAC(testNamespace, api) + assert.NoError(t, err, "Should not return error if old rbac does not exist") + + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: oldRole.Name, + Namespace: testNamespace, + }, &rbacv1.Role{}) + assert.NoError(t, err, "Old role should still exist if present initially") + + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: oldRolebinding.Name, + Namespace: testNamespace, + }, &rbacv1.RoleBinding{}) + if assert.Error(t, err, "Expect get old role to return IsNotFound error") { + assert.True(t, k8sErrors.IsNotFound(err), "Expect error to have IsNotFound type") + } +} + +func TestRemovesOldRoleWhenNewRoleNotPresent(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + api := getTestClusterAPI(t, oldRole, oldRolebinding, newRolebinding) + // Expect two calls to be required: 1. delete role, 2. return nil + err := cleanupDeprecatedRBAC(testNamespace, api) + retryErr := &RetryError{} + if assert.ErrorAs(t, err, &retryErr, "Error should be of type RetryErr") { + assert.Contains(t, err.Error(), "deleted deprecated DevWorkspace Role") + } + err = cleanupDeprecatedRBAC(testNamespace, api) + assert.NoError(t, err, "Should not return error if old rbac does not exist") + + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: oldRole.Name, + Namespace: testNamespace, + }, &rbacv1.Role{}) + if assert.Error(t, err, "Expect get old role to return IsNotFound error") { + assert.True(t, k8sErrors.IsNotFound(err), "Expect error to have IsNotFound type") + } + + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: oldRolebinding.Name, + Namespace: testNamespace, + }, &rbacv1.RoleBinding{}) + assert.NoError(t, err, "Old rolebinding should still exist if present initially") +} diff --git a/pkg/provision/workspace/rbac/role_test.go b/pkg/provision/workspace/rbac/role_test.go new file mode 100644 index 000000000..389e98f1a --- /dev/null +++ b/pkg/provision/workspace/rbac/role_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2019-2022 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rbac + +import ( + "testing" + + "github.com/devfile/devworkspace-operator/pkg/common" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/pkg/infrastructure" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestCreatesRoleIfNotExists(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspace("test-devworkspace") + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := syncRoles(testdw, api) + retryErr := &RetryError{} + if assert.Error(t, err, "Should return RetryError to indicate that role was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRoles(testdw, api) + assert.NoError(t, err, "Should not return error if role is in sync") + actualRole := &rbacv1.Role{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceRoleName(), + Namespace: testNamespace, + }, actualRole) + assert.NoError(t, err, "Role should be created") +} + +func TestDoesNothingIfRoleAlreadyInSync(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspace("test-devworkspace") + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := syncRoles(testdw, api) + retryErr := &RetryError{} + if assert.Error(t, err, "Should return RetryError to indicate that role was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRoles(testdw, api) + assert.NoError(t, err, "Should not return error if role is in sync") + actualRole := &rbacv1.Role{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceRoleName(), + Namespace: testNamespace, + }, actualRole) + assert.NoError(t, err, "Role should be created") + err = syncRoles(testdw, api) + assert.NoError(t, err, "Should not return error if role is in sync") +} + +func TestCreatesSCCRoleIfNotExists(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + testdw := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + api := getTestClusterAPI(t, testdw.DevWorkspace) + retryErr := &RetryError{} + err := syncRoles(testdw, api) + if assert.Error(t, err, "Should return RetryError to indicate that role was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRoles(testdw, api) + if assert.Error(t, err, "Should return RetryError to indicate that SCC role was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRoles(testdw, api) + assert.NoError(t, err, "Should not return error if roles are in sync") + actualRole := &rbacv1.Role{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceSCCRoleName(testSCCName), + Namespace: testNamespace, + }, actualRole) + assert.NoError(t, err, "Role should be created") +} + +func TestDoesNothingIfSCCRoleAlreadyInSync(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + testdw := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + api := getTestClusterAPI(t, testdw.DevWorkspace) + retryErr := &RetryError{} + err := syncRoles(testdw, api) + if assert.Error(t, err, "Should return RetryError to indicate that role was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRoles(testdw, api) + if assert.Error(t, err, "Should return RetryError to indicate that SCC role was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRoles(testdw, api) + assert.NoError(t, err, "Should not return error if roles are in sync") + actualRole := &rbacv1.Role{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceSCCRoleName(testSCCName), + Namespace: testNamespace, + }, actualRole) + assert.NoError(t, err, "Role should be created") + err = syncRoles(testdw, api) + assert.NoError(t, err, "Should not return error if role is in sync") +} diff --git a/pkg/provision/workspace/rbac/rolebinding_test.go b/pkg/provision/workspace/rbac/rolebinding_test.go new file mode 100644 index 000000000..4f3144252 --- /dev/null +++ b/pkg/provision/workspace/rbac/rolebinding_test.go @@ -0,0 +1,154 @@ +// Copyright (c) 2019-2022 Red Hat, Inc. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rbac + +import ( + "testing" + + "github.com/devfile/devworkspace-operator/pkg/common" + "github.com/devfile/devworkspace-operator/pkg/constants" + "github.com/devfile/devworkspace-operator/pkg/infrastructure" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestCreatesRolebindingIfNotExists(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspace("test-devworkspace") + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := syncRolebindings(testdw, api) + retryErr := &RetryError{} + if assert.Error(t, err, "Should return RetryError to indicate that rolebinding was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw, api) + assert.NoError(t, err, "Should not return error if rolebinding is in sync") + actualRB := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceRolebindingName(), + Namespace: testNamespace, + }, actualRB) + assert.NoError(t, err, "Rolebinding should be created") + assert.Equal(t, common.WorkspaceRoleName(), actualRB.RoleRef.Name, "Rolebinding shold reference default role") + expectedSAName := common.ServiceAccountName(testdw.Status.DevWorkspaceId) + assert.True(t, testHasSubject(expectedSAName, testNamespace, actualRB), "Created rolebinding should have workspace SA as subject") +} + +func TestAddsMultipleSubjectsToRolebinding(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + testdw := getTestDevWorkspace("test-devworkspace") + testdw2 := getTestDevWorkspace("test-devworkspace-2") + api := getTestClusterAPI(t, testdw.DevWorkspace) + err := syncRolebindings(testdw, api) + retryErr := &RetryError{} + if assert.Error(t, err, "Should return RetryError to indicate that rolebinding was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw, api) + assert.NoError(t, err, "Should not return error if rolebinding is in sync") + err = syncRolebindings(testdw2, api) + if assert.Error(t, err, "Should return RetryError to indicate that rolebinding was updated") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw2, api) + assert.NoError(t, err, "Should not return error if rolebinding is in sync") + + actualRB := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceRolebindingName(), + Namespace: testNamespace, + }, actualRB) + assert.NoError(t, err, "Rolebinding should be created") + assert.Equal(t, common.WorkspaceRoleName(), actualRB.RoleRef.Name, "Rolebinding shold reference default role") + expectedSAName := common.ServiceAccountName(testdw.Status.DevWorkspaceId) + assert.True(t, testHasSubject(expectedSAName, testNamespace, actualRB), "Created rolebinding should have both workspace SAs as subjects") + expectedSAName2 := common.ServiceAccountName(testdw2.Status.DevWorkspaceId) + assert.True(t, testHasSubject(expectedSAName2, testNamespace, actualRB), "Created rolebinding should have both workspace SAs as subjects") +} + +func TestCreatesSCCRolebindingIfNotExists(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + testdw := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + api := getTestClusterAPI(t, testdw.DevWorkspace) + retryErr := &RetryError{} + err := syncRolebindings(testdw, api) + if assert.Error(t, err, "Should return RetryError to indicate that default rolebinding was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw, api) + if assert.Error(t, err, "Should return RetryError to indicate that SCC rolebinding was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw, api) + assert.NoError(t, err, "Should not return error if rolebindings are in sync") + actualRB := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceSCCRolebindingName(testSCCName), + Namespace: testNamespace, + }, actualRB) + assert.NoError(t, err, "Rolebinding should be created") + assert.Equal(t, common.WorkspaceSCCRoleName(testSCCName), actualRB.RoleRef.Name, "Rolebinding shold reference default role") + expectedSAName := common.ServiceAccountName(testdw.Status.DevWorkspaceId) + assert.True(t, testHasSubject(expectedSAName, testNamespace, actualRB), "Created rolebinding should have workspace SA as subject") +} + +func TestAddsMultipleSubjectsToSCCRolebinding(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + testdw := getTestDevWorkspaceWithAttributes(t, "test-devworkspace", constants.WorkspaceSCCAttribute, testSCCName) + testdw2 := getTestDevWorkspaceWithAttributes(t, "test-devworkspace-2", constants.WorkspaceSCCAttribute, testSCCName) + api := getTestClusterAPI(t, testdw.DevWorkspace) + retryErr := &RetryError{} + err := syncRolebindings(testdw, api) + if assert.Error(t, err, "Should return RetryError to indicate that default rolebinding was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw, api) + if assert.Error(t, err, "Should return RetryError to indicate that SCC rolebinding was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw, api) + assert.NoError(t, err, "Should not return error if rolebindings are in sync") + err = syncRolebindings(testdw2, api) + if assert.Error(t, err, "Should return RetryError to indicate that default rolebinding was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw2, api) + if assert.Error(t, err, "Should return RetryError to indicate that SCC rolebinding was created") { + assert.ErrorAs(t, err, &retryErr, "Error should have RetryError type") + } + err = syncRolebindings(testdw2, api) + assert.NoError(t, err, "Should not return error if rolebindings are in sync") + + actualRB := &rbacv1.RoleBinding{} + err = api.Client.Get(api.Ctx, types.NamespacedName{ + Name: common.WorkspaceSCCRolebindingName(testSCCName), + Namespace: testNamespace, + }, actualRB) + assert.NoError(t, err, "Rolebinding should be created") + assert.Equal(t, common.WorkspaceSCCRoleName(testSCCName), actualRB.RoleRef.Name, "Rolebinding shold reference default role") + expectedSAName := common.ServiceAccountName(testdw.Status.DevWorkspaceId) + assert.True(t, testHasSubject(expectedSAName, testNamespace, actualRB), "Created SCC rolebinding should have both workspace SAs as subjects") + expectedSAName2 := common.ServiceAccountName(testdw2.Status.DevWorkspaceId) + assert.True(t, testHasSubject(expectedSAName2, testNamespace, actualRB), "Created SCC rolebinding should have both workspace SAs as subjects") +} + +func testHasSubject(subjName, namespace string, rolebinding *rbacv1.RoleBinding) bool { + for _, subject := range rolebinding.Subjects { + if subject.Name == subjName && subject.Namespace == namespace && subject.Kind == rbacv1.ServiceAccountKind { + return true + } + } + return false +}