diff --git a/pkg/assets/kubesaw-admins.go b/pkg/assets/kubesaw_admins.go similarity index 79% rename from pkg/assets/kubesaw-admins.go rename to pkg/assets/kubesaw_admins.go index 734043d..3bd1e0e 100644 --- a/pkg/assets/kubesaw-admins.go +++ b/pkg/assets/kubesaw_admins.go @@ -1,5 +1,7 @@ package assets +import "k8s.io/utils/strings/slices" + type KubeSawAdmins struct { Clusters Clusters `yaml:"clusters"` ServiceAccounts []ServiceAccount `yaml:"serviceAccounts"` @@ -43,6 +45,21 @@ type ServiceAccount struct { type Selector struct { // SkipMembers can contain a list of member cluster names the entity shouldn't be applied for SkipMembers []string `yaml:"skipMembers,omitempty"` + // MemberClusters defines a list of member cluster names the entity should be applied for + MemberClusters []string `yaml:"memberClusters,omitempty"` +} + +func (s Selector) ShouldBeSkippedForMember(memberName string) bool { + // should be skipped if the specific member cluster name is provided + // and + // the name is listed in the skipped members + if memberName != "" && slices.Contains(s.SkipMembers, memberName) { + return true + } + // should be skipped if there is at least one selected member cluster + // and + // the name is either empty or is not specified in the selected member clusters + return len(s.MemberClusters) > 0 && (memberName == "" || !slices.Contains(s.MemberClusters, memberName)) } type User struct { diff --git a/pkg/assets/kubesaw_admins_test.go b/pkg/assets/kubesaw_admins_test.go new file mode 100644 index 0000000..1310ab0 --- /dev/null +++ b/pkg/assets/kubesaw_admins_test.go @@ -0,0 +1,66 @@ +package assets + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShouldBeSkippedForMember1(t *testing.T) { + // given + member1Member2 := []string{"member1", "member2"} + member2Member3 := []string{"member2", "member3"} + testCases := map[string]struct { + s Selector + shouldBeSkipped bool + }{ + "no selector": {s: Selector{}, shouldBeSkipped: false}, + "different selected members": {s: Selector{MemberClusters: member2Member3}, shouldBeSkipped: true}, + "in selected members": {s: Selector{MemberClusters: member1Member2}, shouldBeSkipped: false}, + "listed in skipped members": {s: Selector{SkipMembers: member1Member2}, shouldBeSkipped: true}, + "not listed in skipped members": {s: Selector{SkipMembers: member2Member3}, shouldBeSkipped: false}, + "in selected members, but listed in skipped": { + s: Selector{MemberClusters: member1Member2, SkipMembers: member1Member2}, shouldBeSkipped: true}, + "in selected members, not listed in skipped": { + s: Selector{MemberClusters: member1Member2, SkipMembers: member2Member3}, shouldBeSkipped: false}, + "different selected members, not listed in skipped": { + s: Selector{MemberClusters: member2Member3, SkipMembers: member2Member3}, shouldBeSkipped: true}, + "different selected members, and listed in skipped": { + s: Selector{MemberClusters: member2Member3, SkipMembers: member1Member2}, shouldBeSkipped: true}, + } + + for testName, data := range testCases { + t.Run(testName, func(t *testing.T) { + // when + shouldBeSkipped := data.s.ShouldBeSkippedForMember("member1") + + // then + assert.Equal(t, data.shouldBeSkipped, shouldBeSkipped) + }) + } +} + +func TestShouldBeSkippedForEmptyName(t *testing.T) { + // given + member1Member2 := []string{"member1", "member2"} + testCases := map[string]struct { + s Selector + shouldBeSkipped bool + }{ + "no selector": {s: Selector{}, shouldBeSkipped: false}, + "some selected members": {s: Selector{MemberClusters: member1Member2}, shouldBeSkipped: true}, + "some skipped members": {s: Selector{SkipMembers: member1Member2}, shouldBeSkipped: false}, + "some selected members and some skipped members": { + s: Selector{MemberClusters: member1Member2, SkipMembers: member1Member2}, shouldBeSkipped: true}, + } + + for testName, data := range testCases { + t.Run(testName, func(t *testing.T) { + // when + shouldBeSkipped := data.s.ShouldBeSkippedForMember("") + + // then + assert.Equal(t, data.shouldBeSkipped, shouldBeSkipped) + }) + } +} diff --git a/pkg/cmd/generate/admin-manifests_test.go b/pkg/cmd/generate/admin-manifests_test.go index 17792ad..c808a8c 100644 --- a/pkg/cmd/generate/admin-manifests_test.go +++ b/pkg/cmd/generate/admin-manifests_test.go @@ -34,7 +34,10 @@ func TestAdminManifests(t *testing.T) { WithSkippedMembers("member2"), Sa("bob", "", HostRoleBindings("toolchain-host-operator", Role("restart-deployment"), ClusterRole("edit")), - MemberRoleBindings("toolchain-member-operator", Role("restart-deployment"), ClusterRole("edit")))), + MemberRoleBindings("toolchain-member-operator", Role("restart-deployment"), ClusterRole("edit"))), + Sa("jenny", "", + MemberRoleBindings("toolchain-member-operator", Role("restart-deployment"), ClusterRole("view"))). + WithSelectedMembers("member2")), Users( User("john-user", []string{"12345"}, false, "crtadmins-view", HostRoleBindings("toolchain-host-operator", Role("register-cluster"), ClusterRole("edit")), @@ -42,7 +45,10 @@ func TestAdminManifests(t *testing.T) { WithSkippedMembers("member2"), User("bob-crtadmin", []string{"67890"}, false, "crtadmins-exec", HostRoleBindings("toolchain-host-operator", Role("restart-deployment"), ClusterRole("admin")), - MemberRoleBindings("toolchain-member-operator", Role("restart-deployment"), ClusterRole("admin"))))) + MemberRoleBindings("toolchain-member-operator", Role("restart-deployment"), ClusterRole("admin"))), + User("jenny-crtadmin", []string{"98765"}, false, "crtadmins-exec", + MemberRoleBindings("toolchain-member-operator", Role("restart-deployment"), ClusterRole("view"))). + WithSelectedMembers("member2"))) kubeSawAdmins.DefaultServiceAccountsNamespace.Host = "kubesaw-sre-host" kubeSawAdminsContent, err := yaml.Marshal(kubeSawAdmins) require.NoError(t, err) @@ -274,6 +280,11 @@ func verifyServiceAccounts(t *testing.T, outDir, expectedRootDir string, cluster assertSa(saNs, "john"). hasRole(roleNs, clusterType.AsSuffix("install-operator"), clusterType.AsSuffix("install-operator-john")). hasNsClusterRole(roleNs, "admin", clusterType.AsSuffix("clusterrole-admin-john")) + } else { + inKStructure(t, outDir, expectedRootDir). + assertSa(saNs, "jenny"). + hasRole(roleNs, clusterType.AsSuffix("restart-deployment"), clusterType.AsSuffix("restart-deployment-jenny")). + hasNsClusterRole(roleNs, "view", clusterType.AsSuffix("clusterrole-view-jenny")) } inKStructure(t, outDir, expectedRootDir). assertSa(saNs, "bob"). @@ -302,6 +313,14 @@ func verifyUsers(t *testing.T, outDir, expectedRootDir string, clusterType confi // crtadmins-view group is not generated for member2 at all bobsExtraGroupsUserIsNotPartOf = extraGroupsUserIsNotPartOf("crtadmins-view") + } else { + inKStructure(t, outDir, rootDir). + assertUser("jenny-crtadmin"). + hasIdentity("98765") + + newPermissionAssertion(storageAssertion, "", "jenny-crtadmin", "User"). + hasRole(ns, clusterType.AsSuffix("restart-deployment"), clusterType.AsSuffix("restart-deployment-jenny-crtadmin")). + hasNsClusterRole(ns, "view", clusterType.AsSuffix("clusterrole-view-jenny-crtadmin")) } inKStructure(t, outDir, rootDir). diff --git a/pkg/cmd/generate/cli_configs.go b/pkg/cmd/generate/cli_configs.go index 4e39fb0..e987c70 100644 --- a/pkg/cmd/generate/cli_configs.go +++ b/pkg/cmd/generate/cli_configs.go @@ -22,7 +22,6 @@ import ( "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/scheme" "k8s.io/utils/pointer" - "k8s.io/utils/strings/slices" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -185,7 +184,7 @@ func generateForCluster(ctx *generateContext, clusterType configuration.ClusterT tokenPerSAName := tokenPerSA{} for _, sa := range ctx.kubeSawAdmins.ServiceAccounts { - if slices.Contains(sa.Selector.SkipMembers, clusterName) { + if clusterType == configuration.Member && sa.Selector.ShouldBeSkippedForMember(clusterName) { continue } for saClusterType := range sa.PermissionsPerClusterType { diff --git a/pkg/cmd/generate/cli_configs_test.go b/pkg/cmd/generate/cli_configs_test.go index a42727e..df18452 100644 --- a/pkg/cmd/generate/cli_configs_test.go +++ b/pkg/cmd/generate/cli_configs_test.go @@ -37,6 +37,10 @@ func TestGenerateCliConfigs(t *testing.T) { HostRoleBindings("toolchain-host-operator", Role("install-operator"), ClusterRole("admin")), MemberRoleBindings("toolchain-member-operator", Role("install-operator"), ClusterRole("admin"))). WithSkippedMembers("member2"), + Sa("jenny", "", + HostRoleBindings("toolchain-host-operator", Role("restart-deployment"), ClusterRole("view")), + MemberRoleBindings("toolchain-member-operator", Role("restart-deployment"), ClusterRole("view"))). + WithSelectedMembers("member2"), Sa("bob", "", HostRoleBindings("toolchain-host-operator", Role("restart=restart-deployment"), ClusterRole("restart=edit")), MemberRoleBindings("toolchain-member-operator", Role("restart=restart-deployment"), ClusterRole("restart=edit")))), @@ -54,6 +58,7 @@ func TestGenerateCliConfigs(t *testing.T) { setupGockForServiceAccounts(t, HostServerAPI, 50, newServiceAccount("kubesaw-sre-host", "john"), newServiceAccount("kubesaw-sre-host", "bob"), + newServiceAccount("kubesaw-sre-host", "jenny"), ) setupGockForServiceAccounts(t, Member1ServerAPI, 50, newServiceAccount("kubesaw-admins-member", "john"), @@ -61,6 +66,7 @@ func TestGenerateCliConfigs(t *testing.T) { ) setupGockForServiceAccounts(t, Member2ServerAPI, 50, newServiceAccount("kubesaw-admins-member", "bob"), + newServiceAccount("kubesaw-admins-member", "jenny"), ) t.Cleanup(gock.OffAll) @@ -87,7 +93,8 @@ func TestGenerateCliConfigs(t *testing.T) { verifyKsctlConfigFiles(t, tempDir, cliConfigForUser("john", hasHost(), hasMember("member1", "member1")), - cliConfigForUser("bob", hasHost(), hasMember("member1", "member1"), hasMember("member2", "member2"))) + cliConfigForUser("bob", hasHost(), hasMember("member1", "member1"), hasMember("member2", "member2")), + cliConfigForUser("jenny", hasHost(), hasMember("member2", "member2"))) }) t.Run("when there SAs are defined for host cluster only", func(t *testing.T) { @@ -127,6 +134,7 @@ func TestGenerateCliConfigs(t *testing.T) { setupGockForServiceAccounts(t, HostServerAPI, 50, newServiceAccount("kubesaw-admins-member", "john"), newServiceAccount("kubesaw-admins-member", "bob"), + newServiceAccount("kubesaw-admins-member", "jenny"), ) tempDir, err := os.MkdirTemp("", "ksctl-out-") require.NoError(t, err) @@ -141,7 +149,8 @@ func TestGenerateCliConfigs(t *testing.T) { verifyKsctlConfigFiles(t, tempDir, cliConfigForUser("john", hasHost(), hasMember("member1", "host")), - cliConfigForUser("bob", hasHost(), hasMember("member1", "host"), hasMember("member2", "host"))) + cliConfigForUser("bob", hasHost(), hasMember("member1", "host"), hasMember("member2", "host")), + cliConfigForUser("jenny", hasHost(), hasMember("member2", "host"))) }) }) diff --git a/pkg/cmd/generate/cluster.go b/pkg/cmd/generate/cluster.go index 1a2711a..b34b5f3 100644 --- a/pkg/cmd/generate/cluster.go +++ b/pkg/cmd/generate/cluster.go @@ -2,7 +2,6 @@ package generate import ( "github.com/kubesaw/ksctl/pkg/configuration" - "k8s.io/utils/strings/slices" ) type clusterContext struct { @@ -16,7 +15,7 @@ type clusterContext struct { func ensureServiceAccounts(ctx *clusterContext, objsCache objectsCache) error { ctx.Printlnf("-> Ensuring ServiceAccounts and its RoleBindings...") for _, sa := range ctx.kubeSawAdmins.ServiceAccounts { - if ctx.specificKMemberName != "" && slices.Contains(sa.Selector.SkipMembers, ctx.specificKMemberName) { + if sa.Selector.ShouldBeSkippedForMember(ctx.specificKMemberName) { continue } @@ -47,7 +46,7 @@ func ensureUsers(ctx *clusterContext, objsCache objectsCache) error { ctx.Printlnf("-> Ensuring Users and its RoleBindings...") for _, user := range ctx.kubeSawAdmins.Users { - if ctx.specificKMemberName != "" && slices.Contains(user.Selector.SkipMembers, ctx.specificKMemberName) { + if user.Selector.ShouldBeSkippedForMember(ctx.specificKMemberName) { continue } m := &permissionsManager{ diff --git a/pkg/test/environment_config.go b/pkg/test/environment_config.go index 03ec13f..16650f1 100644 --- a/pkg/test/environment_config.go +++ b/pkg/test/environment_config.go @@ -77,6 +77,14 @@ func (c ServiceAccountCreator) WithSkippedMembers(members ...string) ServiceAcco } } +func (c ServiceAccountCreator) WithSelectedMembers(members ...string) ServiceAccountCreator { + return func() assets.ServiceAccount { + serviceAccount := c() + serviceAccount.Selector.MemberClusters = members + return serviceAccount + } +} + func NewPermissionsPerClusterType(permissions ...PermissionsPerClusterTypeModifier) assets.PermissionsPerClusterType { perm := map[string]assets.PermissionBindings{} for _, addPermissions := range permissions { @@ -179,3 +187,11 @@ func (c UserCreator) WithSkippedMembers(members ...string) UserCreator { return user } } + +func (c UserCreator) WithSelectedMembers(members ...string) UserCreator { + return func() assets.User { + user := c() + user.Selector.MemberClusters = members + return user + } +}