From 5c7a47d031b02c76d7d8e2f31564d07beb5cda2d Mon Sep 17 00:00:00 2001 From: Eric Stroczynski Date: Fri, 31 Jul 2020 16:08:28 -0700 Subject: [PATCH] split CSV and non-CSV RBAC using collector.Manifests method --- .../operator-sdk/generate/bundle/bundle.go | 2 +- .../generate/internal/manifests.go | 45 ++ .../packagemanifests/packagemanifests.go | 2 +- .../clusterserviceversion_updaters.go | 88 ++-- .../collector/clusterserviceversion.go | 222 ++++++++++ .../collector/clusterserviceversion_test.go | 296 ++++++++++++++ internal/generate/collector/collect.go | 367 ----------------- .../collector/collector_suite_test.go | 27 ++ internal/generate/collector/manifests.go | 385 +++++++++++++++--- 9 files changed, 954 insertions(+), 480 deletions(-) create mode 100644 internal/cmd/operator-sdk/generate/internal/manifests.go create mode 100644 internal/generate/collector/clusterserviceversion.go create mode 100644 internal/generate/collector/clusterserviceversion_test.go delete mode 100644 internal/generate/collector/collect.go create mode 100644 internal/generate/collector/collector_suite_test.go diff --git a/internal/cmd/operator-sdk/generate/bundle/bundle.go b/internal/cmd/operator-sdk/generate/bundle/bundle.go index 26511e27aec..a0326cee391 100644 --- a/internal/cmd/operator-sdk/generate/bundle/bundle.go +++ b/internal/cmd/operator-sdk/generate/bundle/bundle.go @@ -197,7 +197,7 @@ func (c bundleCmd) runManifests(cfg *config.Config) (err error) { return fmt.Errorf("error generating ClusterServiceVersion: %v", err) } - objs := col.GetNonCSVObjects() + objs := genutil.GetManifestObjects(col) if c.stdout { if err := genutil.WriteObjects(stdout, objs...); err != nil { return err diff --git a/internal/cmd/operator-sdk/generate/internal/manifests.go b/internal/cmd/operator-sdk/generate/internal/manifests.go new file mode 100644 index 00000000000..8a875506cc9 --- /dev/null +++ b/internal/cmd/operator-sdk/generate/internal/manifests.go @@ -0,0 +1,45 @@ +// Copyright 2020 The Operator-SDK Authors +// +// 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 genutil + +import ( + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/operator-framework/operator-sdk/internal/generate/collector" +) + +// GetManifestObjects returns all objects to be written to a manifests directory from collector.Manifests. +func GetManifestObjects(c *collector.Manifests) (objs []controllerutil.Object) { + // All CRDs passed in should be written. + for i := range c.V1CustomResourceDefinitions { + objs = append(objs, &c.V1CustomResourceDefinitions[i]) + } + for i := range c.V1beta1CustomResourceDefinitions { + objs = append(objs, &c.V1beta1CustomResourceDefinitions[i]) + } + + // All ServiceAccounts passed in should be written. + for i := range c.ServiceAccounts { + objs = append(objs, &c.ServiceAccounts[i]) + } + + // RBAC objects that are not a part of the CSV should be written. + _, roleObjs := c.SplitCSVPermissionsObjects() + objs = append(objs, roleObjs...) + _, clusterRoleObjs := c.SplitCSVClusterPermissionsObjects() + objs = append(objs, clusterRoleObjs...) + + return objs +} diff --git a/internal/cmd/operator-sdk/generate/packagemanifests/packagemanifests.go b/internal/cmd/operator-sdk/generate/packagemanifests/packagemanifests.go index 9f6d4120659..365355867dc 100644 --- a/internal/cmd/operator-sdk/generate/packagemanifests/packagemanifests.go +++ b/internal/cmd/operator-sdk/generate/packagemanifests/packagemanifests.go @@ -189,7 +189,7 @@ func (c packagemanifestsCmd) run(cfg *config.Config) error { } if c.updateObjects { - objs := col.GetNonCSVObjects() + objs := genutil.GetManifestObjects(col) if c.stdout { if err := genutil.WriteObjects(stdout, objs...); err != nil { return err diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go b/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go index fd951b0436a..c9dcfaf5c4c 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_updaters.go @@ -86,24 +86,32 @@ const defaultServiceAccountName = "default" // applyRoles applies Roles to strategy's permissions field by combining Roles bound to ServiceAccounts // into one set of permissions. func applyRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) { //nolint:dupl - // Create a set of all service accounts for deployments. These are the only CSV-relevant service accounts. - saNamesToRoleNames := make(map[string]map[string]struct{}) + objs, _ := c.SplitCSVPermissionsObjects() + roleSet := make(map[string]*rbacv1.Role) + for i := range objs { + switch t := objs[i].(type) { + case *rbacv1.Role: + roleSet[t.GetName()] = t + } + } + + saToPermissions := make(map[string]operatorsv1alpha1.StrategyDeploymentPermissions) for _, dep := range c.Deployments { saName := dep.Spec.Template.Spec.ServiceAccountName if saName == "" { saName = defaultServiceAccountName } - saNamesToRoleNames[saName] = make(map[string]struct{}) + saToPermissions[saName] = operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName} } // Collect all role names by their corresponding service accounts via bindings. This lets us // look up all service accounts a role is bound to and create one set of permissions per service account. for _, binding := range c.RoleBindings { - roleRef := binding.RoleRef - if roleRef.Kind == "Role" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) { - for _, name := range getSubjectServiceAccountNames(binding.Subjects) { - if _, hasName := saNamesToRoleNames[name]; hasName { - saNamesToRoleNames[name][roleRef.Name] = struct{}{} + if role, hasRole := roleSet[binding.RoleRef.Name]; hasRole { + for _, subject := range binding.Subjects { + if perm, hasSA := saToPermissions[subject.Name]; hasSA && subject.Kind == "ServiceAccount" { + perm.Rules = append(perm.Rules, role.Rules...) + saToPermissions[subject.Name] = perm } } } @@ -111,14 +119,8 @@ func applyRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDeta // Apply relevant roles to each service account. perms := []operatorsv1alpha1.StrategyDeploymentPermissions{} - for saName, roleNames := range saNamesToRoleNames { - p := operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName} - for _, role := range c.Roles { - if _, ok := roleNames[role.GetName()]; ok { - p.Rules = append(p.Rules, role.Rules...) - } - } - perms = append(perms, p) + for _, perm := range saToPermissions { + perms = append(perms, perm) } strategy.Permissions = perms } @@ -126,51 +128,43 @@ func applyRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDeta // applyClusterRoles applies ClusterRoles to strategy's clusterPermissions field by combining ClusterRoles // bound to ServiceAccounts into one set of clusterPermissions. func applyClusterRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) { //nolint:dupl - // Create a set of all service accounts for deployments. These are the only CSV-relevant service accounts. - saNamesToClusterRoleNames := make(map[string]map[string]struct{}) + objs, _ := c.SplitCSVClusterPermissionsObjects() + roleSet := make(map[string]*rbacv1.ClusterRole) + for i := range objs { + switch t := objs[i].(type) { + case *rbacv1.ClusterRole: + roleSet[t.GetName()] = t + } + } + + saToPermissions := make(map[string]operatorsv1alpha1.StrategyDeploymentPermissions) for _, dep := range c.Deployments { saName := dep.Spec.Template.Spec.ServiceAccountName if saName == "" { saName = defaultServiceAccountName } - saNamesToClusterRoleNames[saName] = make(map[string]struct{}) + saToPermissions[saName] = operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName} } - // Collect all cluster role names by their corresponding service accounts via bindings. This lets us - // look up all service accounts a cluster role is bound to and create one set of permissions per service account. + // Collect all role names by their corresponding service accounts via bindings. This lets us + // look up all service accounts a role is bound to and create one set of permissions per service account. for _, binding := range c.ClusterRoleBindings { - roleRef := binding.RoleRef - if roleRef.Kind == "ClusterRole" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) { - for _, name := range getSubjectServiceAccountNames(binding.Subjects) { - if _, hasName := saNamesToClusterRoleNames[name]; hasName { - saNamesToClusterRoleNames[name][roleRef.Name] = struct{}{} + if role, hasRole := roleSet[binding.RoleRef.Name]; hasRole { + for _, subject := range binding.Subjects { + if perm, hasSA := saToPermissions[subject.Name]; hasSA && subject.Kind == "ServiceAccount" { + perm.Rules = append(perm.Rules, role.Rules...) + saToPermissions[subject.Name] = perm } } } } - // Apply relevant cluster roles to each service account. - clusterPerms := []operatorsv1alpha1.StrategyDeploymentPermissions{} - for saName, clusterRoleNames := range saNamesToClusterRoleNames { - p := operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName} - for _, clusterRole := range c.ClusterRoles { - if _, ok := clusterRoleNames[clusterRole.GetName()]; ok { - p.Rules = append(p.Rules, clusterRole.Rules...) - } - } - clusterPerms = append(clusterPerms, p) - } - strategy.ClusterPermissions = clusterPerms -} - -// getSubjectServiceAccountNames returns a list of all ServiceAccount subject names. -func getSubjectServiceAccountNames(subjects []rbacv1.Subject) (saNames []string) { - for _, subject := range subjects { - if subject.Kind == "ServiceAccount" { - saNames = append(saNames, subject.Name) - } + // Apply relevant roles to each service account. + perms := []operatorsv1alpha1.StrategyDeploymentPermissions{} + for _, perm := range saToPermissions { + perms = append(perms, perm) } - return saNames + strategy.ClusterPermissions = perms } // applyDeployments updates strategy's deployments with the Deployments diff --git a/internal/generate/collector/clusterserviceversion.go b/internal/generate/collector/clusterserviceversion.go new file mode 100644 index 00000000000..f8bdd67365a --- /dev/null +++ b/internal/generate/collector/clusterserviceversion.go @@ -0,0 +1,222 @@ +// Copyright 2020 The Operator-SDK Authors +// +// 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 collector + +import ( + rbacv1 "k8s.io/api/rbac/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// TODO(estroz): there's a significant amount of code dupliation here, a byproduct of Go's type system. +// However at least a few bits can be refactored so each method is smaller. + +const ( + // This service account exists in every namespace as the default. + defaultServiceAccountName = "default" + + serviceAccountKind = "ServiceAccount" +) + +// SplitCSVPermissionsObjects splits roles that should be written to a CSV as permissions (in) +// from roles and role bindings that should be written directly to the bundle (out). +func (c *Manifests) SplitCSVPermissionsObjects() (in, out []controllerutil.Object) { //nolint:dupl + roleMap := make(map[string]*rbacv1.Role) + for i := range c.Roles { + roleMap[c.Roles[i].GetName()] = &c.Roles[i] + } + roleBindingMap := make(map[string]*rbacv1.RoleBinding) + for i := range c.RoleBindings { + roleBindingMap[c.RoleBindings[i].GetName()] = &c.RoleBindings[i] + } + + // Check for unbound roles. + for roleName, role := range roleMap { + hasRef := false + for _, roleBinding := range roleBindingMap { + roleRef := roleBinding.RoleRef + if roleRef.Kind == "Role" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) { + if roleRef.Name == roleName { + hasRef = true + break + } + } + } + if !hasRef { + out = append(out, role) + delete(roleMap, roleName) + } + } + + // If a role is bound and: + // 1. the binding only has one subject and it is a service account that maps to a deployment service account, + // add the role to in. + // 2. the binding only has one subject and it does not map to a deployment service account or is not a service account, + // add both role and binding to out. + // 3. the binding has more than one subject and: + // a. one of those subjects is a deployment's service account, add both role and binding to out and role to in. + // b. none of those subjects is a service account or maps to a deployment's service account, add both role and binding to out. + deploymentSANames := make(map[string]struct{}) + for _, dep := range c.Deployments { + saName := dep.Spec.Template.Spec.ServiceAccountName + if saName == "" { + saName = defaultServiceAccountName + } + deploymentSANames[saName] = struct{}{} + } + + inRoleNames := make(map[string]struct{}) + outRoleNames := make(map[string]struct{}) + outRoleBindingNames := make(map[string]struct{}) + for _, binding := range c.RoleBindings { + roleRef := binding.RoleRef + if roleRef.Kind == "Role" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) { + numSubjects := len(binding.Subjects) + if numSubjects == 1 { + // cases (1) and (2). + if _, hasSA := deploymentSANames[binding.Subjects[0].Name]; hasSA && binding.Subjects[0].Kind == serviceAccountKind { + inRoleNames[roleRef.Name] = struct{}{} + } else { + outRoleNames[roleRef.Name] = struct{}{} + outRoleBindingNames[binding.GetName()] = struct{}{} + } + } else { + // case (3). + for _, subject := range binding.Subjects { + if _, hasSA := deploymentSANames[subject.Name]; hasSA && subject.Kind == serviceAccountKind { + // case (3a). + inRoleNames[roleRef.Name] = struct{}{} + } + } + // case (3b). + outRoleNames[roleRef.Name] = struct{}{} + outRoleBindingNames[binding.GetName()] = struct{}{} + } + } + } + + for roleName := range inRoleNames { + if role, hasRoleName := roleMap[roleName]; hasRoleName { + in = append(in, role) + } + } + for roleName := range outRoleNames { + if role, hasRoleName := roleMap[roleName]; hasRoleName { + out = append(out, role) + } + } + for roleBindingName := range outRoleBindingNames { + if roleBinding, hasRoleBindingName := roleBindingMap[roleBindingName]; hasRoleBindingName { + out = append(out, roleBinding) + } + } + + return in, out +} + +// SplitCSVClusterPermissionsObjects splits cluster roles that should be written to a CSV as clusterPermissions (in) +// from cluster roles and cluster role bindings that should be written directly to the bundle (out). +func (c *Manifests) SplitCSVClusterPermissionsObjects() (in, out []controllerutil.Object) { //nolint:dupl + roleMap := make(map[string]*rbacv1.ClusterRole) + for i := range c.ClusterRoles { + roleMap[c.ClusterRoles[i].GetName()] = &c.ClusterRoles[i] + } + roleBindingMap := make(map[string]*rbacv1.ClusterRoleBinding) + for i := range c.ClusterRoleBindings { + roleBindingMap[c.ClusterRoleBindings[i].GetName()] = &c.ClusterRoleBindings[i] + } + + // Check for unbound roles. + for roleName, role := range roleMap { + hasRef := false + for _, roleBinding := range roleBindingMap { + roleRef := roleBinding.RoleRef + if roleRef.Kind == "ClusterRole" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) { + if roleRef.Name == roleName { + hasRef = true + break + } + } + } + if !hasRef { + out = append(out, role) + delete(roleMap, roleName) + } + } + + // If a role is bound and: + // 1. the binding only has one subject and it is a service account that maps to a deployment service account, + // add the role to in. + // 2. the binding only has one subject and it does not map to a deployment service account or is not a service account, + // add both role and binding to out. + // 3. the binding has more than one subject and: + // a. one of those subjects is a deployment's service account, add both role and binding to out and role to in. + // b. none of those subjects is a service account or maps to a deployment's service account, add both role and binding to out. + deploymentSANames := make(map[string]struct{}) + for _, dep := range c.Deployments { + saName := dep.Spec.Template.Spec.ServiceAccountName + if saName == "" { + saName = defaultServiceAccountName + } + deploymentSANames[saName] = struct{}{} + } + + inRoleNames := make(map[string]struct{}) + outRoleNames := make(map[string]struct{}) + outRoleBindingNames := make(map[string]struct{}) + for _, binding := range c.ClusterRoleBindings { + roleRef := binding.RoleRef + if roleRef.Kind == "ClusterRole" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) { + numSubjects := len(binding.Subjects) + if numSubjects == 1 { + // cases (1) and (2). + if _, hasSA := deploymentSANames[binding.Subjects[0].Name]; hasSA && binding.Subjects[0].Kind == serviceAccountKind { + inRoleNames[roleRef.Name] = struct{}{} + } else { + outRoleNames[roleRef.Name] = struct{}{} + outRoleBindingNames[binding.GetName()] = struct{}{} + } + } else { + // case (3). + for _, subject := range binding.Subjects { + if _, hasSA := deploymentSANames[subject.Name]; hasSA && subject.Kind == serviceAccountKind { + // case (3a). + inRoleNames[roleRef.Name] = struct{}{} + } + } + // case (3b). + outRoleNames[roleRef.Name] = struct{}{} + outRoleBindingNames[binding.GetName()] = struct{}{} + } + } + } + + for roleName := range inRoleNames { + if role, hasRoleName := roleMap[roleName]; hasRoleName { + in = append(in, role) + } + } + for roleName := range outRoleNames { + if role, hasRoleName := roleMap[roleName]; hasRoleName { + out = append(out, role) + } + } + for roleBindingName := range outRoleBindingNames { + if roleBinding, hasRoleBindingName := roleBindingMap[roleBindingName]; hasRoleBindingName { + out = append(out, roleBinding) + } + } + + return in, out +} diff --git a/internal/generate/collector/clusterserviceversion_test.go b/internal/generate/collector/clusterserviceversion_test.go new file mode 100644 index 00000000000..93ed60a35a5 --- /dev/null +++ b/internal/generate/collector/clusterserviceversion_test.go @@ -0,0 +1,296 @@ +// Copyright 2020 The Operator-SDK Authors +// +// 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 collector + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + rbacv1 "k8s.io/api/rbac/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +var _ = Describe("ClusterServiceVersion", func() { + var ( + c *Manifests + in, out []controllerutil.Object + ) + + BeforeEach(func() { + c = &Manifests{} + }) + + Describe("SplitCSVPermissionsObjects", func() { + + It("should return empty lists for an empty Manifests", func() { + c.Roles = []rbacv1.Role{} + in, out = c.SplitCSVPermissionsObjects() + Expect(in).To(HaveLen(0)) + Expect(out).To(HaveLen(0)) + }) + It("should return non-empty lists", func() { + By("splitting 1 Role no RoleBinding") + c.Roles = []rbacv1.Role{newRole("my-role")} + in, out = c.SplitCSVPermissionsObjects() + Expect(in).To(HaveLen(0)) + Expect(out).To(HaveLen(1)) + Expect(getRoleNames(out)).To(ContainElement("my-role")) + + By("splitting 1 Role 1 RoleBinding with 1 Subject not containing Deployment serviceAccountName") + c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount("my-dep-account")} + c.Roles = []rbacv1.Role{newRole("my-role")} + c.RoleBindings = []rbacv1.RoleBinding{ + newRoleBinding("my-role-binding", newRoleRef("my-role"), newServiceAccountSubject("my-other-account")), + } + in, out = c.SplitCSVPermissionsObjects() + Expect(in).To(HaveLen(0)) + Expect(out).To(HaveLen(2)) + Expect(getRoleNames(out)).To(ContainElement("my-role")) + Expect(getRoleBindingNames(out)).To(ContainElement("my-role-binding")) + + By("splitting 1 Role 1 RoleBinding with 1 Subject containing Deployment serviceAccountName") + c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount("my-dep-account")} + c.Roles = []rbacv1.Role{newRole("my-role")} + c.RoleBindings = []rbacv1.RoleBinding{ + newRoleBinding("my-role-binding", newRoleRef("my-role"), newServiceAccountSubject("my-dep-account")), + } + in, out = c.SplitCSVPermissionsObjects() + Expect(in).To(HaveLen(1)) + Expect(getRoleNames(in)).To(ContainElement("my-role")) + Expect(out).To(HaveLen(0)) + + By("splitting 1 Role 1 RoleBinding with 2 Subjects containing a Deployment serviceAccountName") + c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount("my-dep-account")} + c.Roles = []rbacv1.Role{newRole("my-role")} + c.RoleBindings = []rbacv1.RoleBinding{ + newRoleBinding("my-role-binding", + newRoleRef("my-role"), + newServiceAccountSubject("my-dep-account"), newServiceAccountSubject("my-other-account")), + } + in, out = c.SplitCSVPermissionsObjects() + Expect(in).To(HaveLen(1)) + Expect(getRoleNames(in)).To(ContainElement("my-role")) + Expect(out).To(HaveLen(2)) + Expect(getRoleNames(out)).To(ContainElement("my-role")) + Expect(getRoleBindingNames(out)).To(ContainElement("my-role-binding")) + + By("splitting 2 Roles 2 RoleBinding, one with 1 Subject not containing and the other with 2 Subjects containing a Deployment serviceAccountName") + c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount("my-dep-account")} + c.Roles = []rbacv1.Role{newRole("my-role-1"), newRole("my-role-2")} + c.RoleBindings = []rbacv1.RoleBinding{ + newRoleBinding("my-role-binding-1", + newRoleRef("my-role-1"), + newServiceAccountSubject("my-dep-account"), newServiceAccountSubject("my-other-account")), + newRoleBinding("my-role-binding-2", + newRoleRef("my-role-2"), + newServiceAccountSubject("my-other-account")), + } + in, out = c.SplitCSVPermissionsObjects() + Expect(in).To(HaveLen(1)) + Expect(getRoleNames(in)).To(ContainElement("my-role-1")) + Expect(out).To(HaveLen(4)) + Expect(getRoleNames(out)).To(ContainElement("my-role-1")) + Expect(getRoleNames(out)).To(ContainElement("my-role-2")) + Expect(getRoleBindingNames(out)).To(ContainElement("my-role-binding-1")) + Expect(getRoleBindingNames(out)).To(ContainElement("my-role-binding-2")) + + By("splitting on 2 different Deployments") + c.Deployments = []appsv1.Deployment{ + newDeploymentWithServiceAccount("my-dep-account-1"), + newDeploymentWithServiceAccount("my-dep-account-2"), + } + c.Roles = []rbacv1.Role{newRole("my-role-1"), newRole("my-role-2"), newRole("my-role-3")} + c.RoleBindings = []rbacv1.RoleBinding{ + newRoleBinding("my-role-binding-1", + newRoleRef("my-role-1"), + newServiceAccountSubject("my-dep-account-1"), newServiceAccountSubject("my-other-account")), + newRoleBinding("my-role-binding-2", + newRoleRef("my-role-2"), + newServiceAccountSubject("my-other-account")), + newRoleBinding("my-role-binding-3", + newRoleRef("my-role-3"), + newServiceAccountSubject("my-dep-account-2")), + } + in, out = c.SplitCSVPermissionsObjects() + Expect(in).To(HaveLen(2)) + Expect(getRoleNames(in)).To(ContainElement("my-role-1")) + Expect(getRoleNames(in)).To(ContainElement("my-role-3")) + Expect(out).To(HaveLen(4)) + Expect(getRoleNames(out)).To(ContainElement("my-role-1")) + Expect(getRoleNames(out)).To(ContainElement("my-role-2")) + Expect(getRoleBindingNames(out)).To(ContainElement("my-role-binding-1")) + Expect(getRoleBindingNames(out)).To(ContainElement("my-role-binding-2")) + }) + }) + + Describe("SplitCSVClusterPermissionsObjects", func() { + It("should return empty lists for an empty Manifests", func() { + c.ClusterRoles = []rbacv1.ClusterRole{} + in, out = c.SplitCSVClusterPermissionsObjects() + Expect(in).To(HaveLen(0)) + Expect(out).To(HaveLen(0)) + }) + It("should return non-empty lists", func() { + By("splitting 1 ClusterRole no ClusterRoleBinding") + c.ClusterRoles = []rbacv1.ClusterRole{newClusterRole("my-role")} + in, out = c.SplitCSVClusterPermissionsObjects() + Expect(in).To(HaveLen(0)) + Expect(out).To(HaveLen(1)) + Expect(getClusterRoleNames(out)).To(ContainElement("my-role")) + + By("splitting 1 ClusterRole 1 ClusterRoleBinding with 1 Subject not containing Deployment serviceAccountName") + c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount("my-dep-account")} + c.ClusterRoles = []rbacv1.ClusterRole{newClusterRole("my-role")} + c.ClusterRoleBindings = []rbacv1.ClusterRoleBinding{ + newClusterRoleBinding("my-role-binding", newClusterRoleRef("my-role"), newServiceAccountSubject("my-other-account")), + } + in, out = c.SplitCSVClusterPermissionsObjects() + Expect(in).To(HaveLen(0)) + Expect(out).To(HaveLen(2)) + Expect(getClusterRoleNames(out)).To(ContainElement("my-role")) + Expect(getClusterRoleBindingNames(out)).To(ContainElement("my-role-binding")) + + By("splitting 1 ClusterRole 1 ClusterRoleBinding with 1 Subject containing Deployment serviceAccountName") + c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount("my-dep-account")} + c.ClusterRoles = []rbacv1.ClusterRole{newClusterRole("my-role")} + c.ClusterRoleBindings = []rbacv1.ClusterRoleBinding{ + newClusterRoleBinding("my-role-binding", newClusterRoleRef("my-role"), newServiceAccountSubject("my-dep-account")), + } + in, out = c.SplitCSVClusterPermissionsObjects() + Expect(in).To(HaveLen(1)) + Expect(getClusterRoleNames(in)).To(ContainElement("my-role")) + Expect(out).To(HaveLen(0)) + + By("splitting 1 ClusterRole 1 ClusterRoleBinding with 2 Subjects containing a Deployment serviceAccountName") + c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount("my-dep-account")} + c.ClusterRoles = []rbacv1.ClusterRole{newClusterRole("my-role")} + c.ClusterRoleBindings = []rbacv1.ClusterRoleBinding{ + newClusterRoleBinding("my-role-binding", + newClusterRoleRef("my-role"), + newServiceAccountSubject("my-dep-account"), newServiceAccountSubject("my-other-account")), + } + in, out = c.SplitCSVClusterPermissionsObjects() + Expect(in).To(HaveLen(1)) + Expect(getClusterRoleNames(in)).To(ContainElement("my-role")) + Expect(out).To(HaveLen(2)) + Expect(getClusterRoleNames(out)).To(ContainElement("my-role")) + Expect(getClusterRoleBindingNames(out)).To(ContainElement("my-role-binding")) + + By("splitting 2 ClusterRoles 2 ClusterRoleBindings, one with 1 Subject not containing and the other with 2 Subjects containing a Deployment serviceAccountName") + c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount("my-dep-account")} + c.ClusterRoles = []rbacv1.ClusterRole{newClusterRole("my-role-1"), newClusterRole("my-role-2")} + c.ClusterRoleBindings = []rbacv1.ClusterRoleBinding{ + newClusterRoleBinding("my-role-binding-1", + newClusterRoleRef("my-role-1"), + newServiceAccountSubject("my-dep-account"), newServiceAccountSubject("my-other-account")), + newClusterRoleBinding("my-role-binding-2", + newClusterRoleRef("my-role-2"), + newServiceAccountSubject("my-other-account")), + } + in, out = c.SplitCSVClusterPermissionsObjects() + Expect(in).To(HaveLen(1)) + Expect(getClusterRoleNames(in)).To(ContainElement("my-role-1")) + Expect(out).To(HaveLen(4)) + Expect(getClusterRoleNames(out)).To(ContainElement("my-role-1")) + Expect(getClusterRoleNames(out)).To(ContainElement("my-role-2")) + Expect(getClusterRoleBindingNames(out)).To(ContainElement("my-role-binding-1")) + Expect(getClusterRoleBindingNames(out)).To(ContainElement("my-role-binding-2")) + }) + }) + +}) + +func getRoleNames(objs []controllerutil.Object) []string { + return getNamesForKind("Role", objs) +} + +func getRoleBindingNames(objs []controllerutil.Object) []string { + return getNamesForKind("RoleBinding", objs) +} + +func getClusterRoleNames(objs []controllerutil.Object) []string { + return getNamesForKind("ClusterRole", objs) +} + +func getClusterRoleBindingNames(objs []controllerutil.Object) []string { + return getNamesForKind("ClusterRoleBinding", objs) +} + +func getNamesForKind(kind string, objs []controllerutil.Object) (names []string) { + for _, obj := range objs { + if obj.GetObjectKind().GroupVersionKind().Kind == kind { + names = append(names, obj.GetName()) + } + } + return +} + +func newDeploymentWithServiceAccount(name string) (d appsv1.Deployment) { + d.Spec.Template.Spec.ServiceAccountName = name + return d +} + +func newRole(name string) (r rbacv1.Role) { + r.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + r.SetName(name) + return r +} + +func newClusterRole(name string) (r rbacv1.ClusterRole) { + r.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRole")) + r.SetName(name) + return r +} + +func newRoleBinding(name string, ref rbacv1.RoleRef, subjects ...rbacv1.Subject) (r rbacv1.RoleBinding) { + r.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + r.SetName(name) + r.RoleRef = ref + r.Subjects = subjects + return r +} + +func newClusterRoleBinding(name string, ref rbacv1.RoleRef, subjects ...rbacv1.Subject) (r rbacv1.ClusterRoleBinding) { + r.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) + r.SetName(name) + r.RoleRef = ref + r.Subjects = subjects + return r +} + +func newRef(name, kind, apiGroup string) (s rbacv1.RoleRef) { + s.Name = name + s.Kind = kind + s.APIGroup = apiGroup + return s +} + +func newRoleRef(name string) (s rbacv1.RoleRef) { + return newRef(name, "Role", rbacv1.SchemeGroupVersion.Group) +} + +func newClusterRoleRef(name string) (s rbacv1.RoleRef) { + return newRef(name, "ClusterRole", rbacv1.SchemeGroupVersion.Group) +} + +func newSubject(name, kind string) (s rbacv1.Subject) { + s.Name = name + s.Kind = kind + return s +} + +func newServiceAccountSubject(name string) (s rbacv1.Subject) { + return newSubject(name, "ServiceAccount") +} diff --git a/internal/generate/collector/collect.go b/internal/generate/collector/collect.go deleted file mode 100644 index 71604d5da13..00000000000 --- a/internal/generate/collector/collect.go +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 collector - -import ( - "bytes" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - - scorecardv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" - log "github.com/sirupsen/logrus" - admissionregv1 "k8s.io/api/admissionregistration/v1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/yaml" - - "github.com/operator-framework/operator-sdk/internal/util/k8sutil" -) - -// Manifests holds a collector of all manifests relevant to CSV updates. -type Manifests struct { - Roles []rbacv1.Role - ClusterRoles []rbacv1.ClusterRole - RoleBindings []rbacv1.RoleBinding - ClusterRoleBindings []rbacv1.ClusterRoleBinding - Deployments []appsv1.Deployment - ServiceAccounts []corev1.ServiceAccount - V1CustomResourceDefinitions []apiextv1.CustomResourceDefinition - V1beta1CustomResourceDefinitions []apiextv1beta1.CustomResourceDefinition - ValidatingWebhooks []admissionregv1.ValidatingWebhook - MutatingWebhooks []admissionregv1.MutatingWebhook - CustomResources []unstructured.Unstructured - ScorecardConfig scorecardv1alpha3.Configuration - - Others []unstructured.Unstructured -} - -var ( - roleGK = rbacv1.SchemeGroupVersion.WithKind("Role").GroupKind() - clusterRoleGK = rbacv1.SchemeGroupVersion.WithKind("ClusterRole").GroupKind() - roleBindingGK = rbacv1.SchemeGroupVersion.WithKind("RoleBinding").GroupKind() - clusterRoleBindingGK = rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding").GroupKind() - serviceAccountGK = corev1.SchemeGroupVersion.WithKind("ServiceAccount").GroupKind() - deploymentGK = appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind() - crdGK = apiextv1.SchemeGroupVersion.WithKind("CustomResourceDefinition").GroupKind() - validatingWebhookCfgGK = admissionregv1.SchemeGroupVersion.WithKind("ValidatingWebhookConfiguration").GroupKind() - mutatingWebhookCfgGK = admissionregv1.SchemeGroupVersion.WithKind("MutatingWebhookConfiguration").GroupKind() - v1alpha3ScorecardCfgGK = scorecardv1alpha3.GroupVersion.WithKind("Configuration").GroupKind() -) - -// UpdateFromDirs adds Roles, ClusterRoles, Deployments, and Custom Resource examples -// found in deployDir, and CustomResourceDefinitions found in crdsDir, -// to their respective fields in a Manifests, then filters and deduplicates them. -// All other objects are added to Manifests.Others. -func (c *Manifests) UpdateFromDirs(deployDir, crdsDir string) error { - // Collect all manifests in paths. - err := filepath.Walk(deployDir, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() { - return err - } - - b, err := ioutil.ReadFile(path) - if err != nil { - return err - } - scanner := k8sutil.NewYAMLScanner(bytes.NewBuffer(b)) - for scanner.Scan() { - manifest := scanner.Bytes() - typeMeta, err := k8sutil.GetTypeMetaFromBytes(manifest) - if err != nil { - log.Debugf("No TypeMeta in %s, skipping file", path) - continue - } - - gvk := typeMeta.GroupVersionKind() - switch gvk.GroupKind() { - case roleGK: - err = c.addRoles(manifest) - case clusterRoleGK: - err = c.addClusterRoles(manifest) - case roleBindingGK: - err = c.addRoleBindings(manifest) - case clusterRoleBindingGK: - err = c.addClusterRoleBindings(manifest) - case serviceAccountGK: - err = c.addServiceAccounts(manifest) - case deploymentGK: - err = c.addDeployments(manifest) - case crdGK: - // Skip for now and add explicitly from CRDsDir input. - case validatingWebhookCfgGK: - err = c.addValidatingWebhookConfigurations(manifest) - case mutatingWebhookCfgGK: - err = c.addMutatingWebhookConfigurations(manifest) - case v1alpha3ScorecardCfgGK: - err = c.addScorecardConfig(manifest) - default: - err = c.addOthers(manifest) - } - if err != nil { - return fmt.Errorf("error adding %s to manifest collector: %v", gvk, err) - } - } - return scanner.Err() - }) - if err != nil { - return fmt.Errorf("error collecting manifests from directory %s: %v", deployDir, err) - } - - // Add CRDs from input. - if isDirExist(crdsDir) { - c.V1CustomResourceDefinitions, c.V1beta1CustomResourceDefinitions, err = k8sutil.GetCustomResourceDefinitions(crdsDir) - if err != nil { - return fmt.Errorf("error adding CustomResourceDefinitions to manifest collector: %v", err) - } - } - - // Filter manifests based on data collected. - c.filter() - - // Remove duplicate manifests. - if err := c.deduplicate(); err != nil { - return fmt.Errorf("error removing duplicate manifests: %v", err) - } - - return nil -} - -// UpdateFromReader adds Roles, ClusterRoles, Deployments, CustomResourceDefinitions, -// and Custom Resources found in r to their respective fields in a Manifests, then -// filters and deduplicates them. All other objects are added to Manifests.Others. -func (c *Manifests) UpdateFromReader(r io.Reader) error { - // Bundle contents. - scanner := k8sutil.NewYAMLScanner(r) - for scanner.Scan() { - manifest := scanner.Bytes() - typeMeta, err := k8sutil.GetTypeMetaFromBytes(manifest) - if err != nil { - log.Debug("No TypeMeta found, skipping manifest") - continue - } - - gvk := typeMeta.GroupVersionKind() - switch gvk.GroupKind() { - case roleGK: - err = c.addRoles(manifest) - case clusterRoleGK: - err = c.addClusterRoles(manifest) - case roleBindingGK: - err = c.addRoleBindings(manifest) - case clusterRoleBindingGK: - err = c.addClusterRoleBindings(manifest) - case serviceAccountGK: - err = c.addServiceAccounts(manifest) - case deploymentGK: - err = c.addDeployments(manifest) - case crdGK: - err = c.addCustomResourceDefinitions(gvk.Version, manifest) - case validatingWebhookCfgGK: - err = c.addValidatingWebhookConfigurations(manifest) - case mutatingWebhookCfgGK: - err = c.addMutatingWebhookConfigurations(manifest) - case v1alpha3ScorecardCfgGK: - err = c.addScorecardConfig(manifest) - default: - err = c.addOthers(manifest) - } - if err != nil { - return fmt.Errorf("error adding %s to manifest collector: %v", gvk, err) - } - } - if err := scanner.Err(); err != nil { - return fmt.Errorf("error collecting manifests from reader: %v", err) - } - - // Filter manifests based on data collected. - c.filter() - - // Remove duplicate manifests. - if err := c.deduplicate(); err != nil { - return fmt.Errorf("error removing duplicate manifests: %v", err) - } - - return nil -} - -// addRoles assumes all manifest data in rawManifests are Roles and adds them -// to the collector. -func (c *Manifests) addRoles(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - role := rbacv1.Role{} - if err := yaml.Unmarshal(rawManifest, &role); err != nil { - return err - } - c.Roles = append(c.Roles, role) - } - return nil -} - -// addClusterRoles assumes all manifest data in rawManifests are ClusterRoles -// and adds them to the collector. -func (c *Manifests) addClusterRoles(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - role := rbacv1.ClusterRole{} - if err := yaml.Unmarshal(rawManifest, &role); err != nil { - return err - } - c.ClusterRoles = append(c.ClusterRoles, role) - } - return nil -} - -// addRoleBindings assumes all manifest data in rawManifests are RoleBindings and adds them to the collector. -func (c *Manifests) addRoleBindings(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - binding := rbacv1.RoleBinding{} - if err := yaml.Unmarshal(rawManifest, &binding); err != nil { - return err - } - c.RoleBindings = append(c.RoleBindings, binding) - } - return nil -} - -// addClusterRoleBindings assumes all manifest data in rawManifests are ClusterRoleBindings and adds them to the collector. -func (c *Manifests) addClusterRoleBindings(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - binding := rbacv1.ClusterRoleBinding{} - if err := yaml.Unmarshal(rawManifest, &binding); err != nil { - return err - } - c.ClusterRoleBindings = append(c.ClusterRoleBindings, binding) - } - return nil -} - -// addServiceAccounts assumes all manifest data in rawManifests are ServiceAccounts and adds them to the collector. -func (c *Manifests) addServiceAccounts(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - sa := corev1.ServiceAccount{} - if err := yaml.Unmarshal(rawManifest, &sa); err != nil { - return err - } - c.ServiceAccounts = append(c.ServiceAccounts, sa) - } - return nil -} - -// addDeployments assumes all manifest data in rawManifests are Deployments -// and adds them to the collector. -func (c *Manifests) addDeployments(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - dep := appsv1.Deployment{} - if err := yaml.Unmarshal(rawManifest, &dep); err != nil { - return err - } - c.Deployments = append(c.Deployments, dep) - } - return nil -} - -// addCustomResourceDefinitions assumes all manifest data in rawManifests are -// CustomResourceDefinitions and adds them to the collector. version determines -// which CustomResourceDefinition type is used for all manifests in rawManifests. -func (c *Manifests) addCustomResourceDefinitions(version string, rawManifests ...[]byte) (err error) { - for _, rawManifest := range rawManifests { - switch version { - case apiextv1.SchemeGroupVersion.Version: - crd := apiextv1.CustomResourceDefinition{} - if err := yaml.Unmarshal(rawManifest, &crd); err != nil { - return err - } - c.V1CustomResourceDefinitions = append(c.V1CustomResourceDefinitions, crd) - case apiextv1beta1.SchemeGroupVersion.Version: - crd := apiextv1beta1.CustomResourceDefinition{} - if err := yaml.Unmarshal(rawManifest, &crd); err != nil { - return err - } - c.V1beta1CustomResourceDefinitions = append(c.V1beta1CustomResourceDefinitions, crd) - default: - return fmt.Errorf("unrecognized CustomResourceDefinition version %q", version) - } - } - return nil -} - -// addValidatingWebhookConfigurations assumes all manifest data in rawManifests -// are ValidatingWebhookConfigurations and adds their webhooks to the collector. -func (c *Manifests) addValidatingWebhookConfigurations(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - webhookConfig := admissionregv1.ValidatingWebhookConfiguration{} - if err := yaml.Unmarshal(rawManifest, &webhookConfig); err != nil { - return err - } - c.ValidatingWebhooks = append(c.ValidatingWebhooks, webhookConfig.Webhooks...) - } - return nil -} - -// addMutatingWebhookConfigurations assumes all manifest data in rawManifests -// are MutatingWebhookConfigurations and adds their webhooks to the collector. -func (c *Manifests) addMutatingWebhookConfigurations(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - webhookConfig := admissionregv1.MutatingWebhookConfiguration{} - if err := yaml.Unmarshal(rawManifest, &webhookConfig); err != nil { - return err - } - c.MutatingWebhooks = append(c.MutatingWebhooks, webhookConfig.Webhooks...) - } - return nil -} - -// addScorecardConfig assumes manifest data in rawManifests is a ScorecardConfigs and adds it to the collector. -// If a config has already been found, addScorecardConfig will return an error. -func (c *Manifests) addScorecardConfig(rawManifest []byte) error { - cfg := scorecardv1alpha3.Configuration{} - if err := yaml.Unmarshal(rawManifest, &cfg); err != nil { - return err - } - if c.ScorecardConfig.Metadata.Name != "" { - return errors.New("duplicate scorecard configurations in collector input") - } - c.ScorecardConfig = cfg - return nil -} - -// addOthers assumes all manifest data in rawManifests are able to be -// unmarshalled into an Unstructured object and adds them to the collector. -func (c *Manifests) addOthers(rawManifests ...[]byte) error { - for _, rawManifest := range rawManifests { - u := unstructured.Unstructured{} - if err := yaml.Unmarshal(rawManifest, &u); err != nil { - return err - } - c.Others = append(c.Others, u) - } - return nil -} - -// isDirExist returns true if dir exists on disk. -func isDirExist(dir string) bool { - if dir == "" { - return false - } - info, err := os.Stat(dir) - return (err == nil && info.IsDir()) || os.IsExist(err) -} diff --git a/internal/generate/collector/collector_suite_test.go b/internal/generate/collector/collector_suite_test.go new file mode 100644 index 00000000000..e116be58ab7 --- /dev/null +++ b/internal/generate/collector/collector_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2020 The Operator-SDK Authors +// +// 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 collector + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestGenerator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Collector Suite") +} diff --git a/internal/generate/collector/manifests.go b/internal/generate/collector/manifests.go index ffbe88b0cd6..71604d5da13 100644 --- a/internal/generate/collector/manifests.go +++ b/internal/generate/collector/manifests.go @@ -15,96 +15,353 @@ package collector import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + scorecardv1alpha3 "github.com/operator-framework/api/pkg/apis/scorecard/v1alpha3" + log "github.com/sirupsen/logrus" + admissionregv1 "k8s.io/api/admissionregistration/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" + + "github.com/operator-framework/operator-sdk/internal/util/k8sutil" +) + +// Manifests holds a collector of all manifests relevant to CSV updates. +type Manifests struct { + Roles []rbacv1.Role + ClusterRoles []rbacv1.ClusterRole + RoleBindings []rbacv1.RoleBinding + ClusterRoleBindings []rbacv1.ClusterRoleBinding + Deployments []appsv1.Deployment + ServiceAccounts []corev1.ServiceAccount + V1CustomResourceDefinitions []apiextv1.CustomResourceDefinition + V1beta1CustomResourceDefinitions []apiextv1beta1.CustomResourceDefinition + ValidatingWebhooks []admissionregv1.ValidatingWebhook + MutatingWebhooks []admissionregv1.MutatingWebhook + CustomResources []unstructured.Unstructured + ScorecardConfig scorecardv1alpha3.Configuration + + Others []unstructured.Unstructured +} + +var ( + roleGK = rbacv1.SchemeGroupVersion.WithKind("Role").GroupKind() + clusterRoleGK = rbacv1.SchemeGroupVersion.WithKind("ClusterRole").GroupKind() + roleBindingGK = rbacv1.SchemeGroupVersion.WithKind("RoleBinding").GroupKind() + clusterRoleBindingGK = rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding").GroupKind() + serviceAccountGK = corev1.SchemeGroupVersion.WithKind("ServiceAccount").GroupKind() + deploymentGK = appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind() + crdGK = apiextv1.SchemeGroupVersion.WithKind("CustomResourceDefinition").GroupKind() + validatingWebhookCfgGK = admissionregv1.SchemeGroupVersion.WithKind("ValidatingWebhookConfiguration").GroupKind() + mutatingWebhookCfgGK = admissionregv1.SchemeGroupVersion.WithKind("MutatingWebhookConfiguration").GroupKind() + v1alpha3ScorecardCfgGK = scorecardv1alpha3.GroupVersion.WithKind("Configuration").GroupKind() ) -// This service account exists in every namespace as the default. -const defaultServiceAccountName = "default" +// UpdateFromDirs adds Roles, ClusterRoles, Deployments, and Custom Resource examples +// found in deployDir, and CustomResourceDefinitions found in crdsDir, +// to their respective fields in a Manifests, then filters and deduplicates them. +// All other objects are added to Manifests.Others. +func (c *Manifests) UpdateFromDirs(deployDir, crdsDir string) error { + // Collect all manifests in paths. + err := filepath.Walk(deployDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + + b, err := ioutil.ReadFile(path) + if err != nil { + return err + } + scanner := k8sutil.NewYAMLScanner(bytes.NewBuffer(b)) + for scanner.Scan() { + manifest := scanner.Bytes() + typeMeta, err := k8sutil.GetTypeMetaFromBytes(manifest) + if err != nil { + log.Debugf("No TypeMeta in %s, skipping file", path) + continue + } -func (c *Manifests) GetNonCSVObjects() (objs []controllerutil.Object) { - // All CRDs passed in should be written. - for i := range c.V1CustomResourceDefinitions { - objs = append(objs, &c.V1CustomResourceDefinitions[i]) + gvk := typeMeta.GroupVersionKind() + switch gvk.GroupKind() { + case roleGK: + err = c.addRoles(manifest) + case clusterRoleGK: + err = c.addClusterRoles(manifest) + case roleBindingGK: + err = c.addRoleBindings(manifest) + case clusterRoleBindingGK: + err = c.addClusterRoleBindings(manifest) + case serviceAccountGK: + err = c.addServiceAccounts(manifest) + case deploymentGK: + err = c.addDeployments(manifest) + case crdGK: + // Skip for now and add explicitly from CRDsDir input. + case validatingWebhookCfgGK: + err = c.addValidatingWebhookConfigurations(manifest) + case mutatingWebhookCfgGK: + err = c.addMutatingWebhookConfigurations(manifest) + case v1alpha3ScorecardCfgGK: + err = c.addScorecardConfig(manifest) + default: + err = c.addOthers(manifest) + } + if err != nil { + return fmt.Errorf("error adding %s to manifest collector: %v", gvk, err) + } + } + return scanner.Err() + }) + if err != nil { + return fmt.Errorf("error collecting manifests from directory %s: %v", deployDir, err) } - for i := range c.V1beta1CustomResourceDefinitions { - objs = append(objs, &c.V1beta1CustomResourceDefinitions[i]) + + // Add CRDs from input. + if isDirExist(crdsDir) { + c.V1CustomResourceDefinitions, c.V1beta1CustomResourceDefinitions, err = k8sutil.GetCustomResourceDefinitions(crdsDir) + if err != nil { + return fmt.Errorf("error adding CustomResourceDefinitions to manifest collector: %v", err) + } } - // All ServiceAccounts passed in should be written. - for i := range c.ServiceAccounts { - objs = append(objs, &c.ServiceAccounts[i]) + // Filter manifests based on data collected. + c.filter() + + // Remove duplicate manifests. + if err := c.deduplicate(); err != nil { + return fmt.Errorf("error removing duplicate manifests: %v", err) } - // RBAC objects that are not a part of the CSV should be written separately. - saNamesToRoleNames := make(map[string]map[string]struct{}) - for _, binding := range c.RoleBindings { - roleRef := binding.RoleRef - if roleRef.Kind == "Role" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) { - for _, name := range getSubjectServiceAccountNames(binding.Subjects) { - if _, hasName := saNamesToRoleNames[name]; !hasName { - saNamesToRoleNames[name] = make(map[string]struct{}) - } - saNamesToRoleNames[name][roleRef.Name] = struct{}{} - } + return nil +} + +// UpdateFromReader adds Roles, ClusterRoles, Deployments, CustomResourceDefinitions, +// and Custom Resources found in r to their respective fields in a Manifests, then +// filters and deduplicates them. All other objects are added to Manifests.Others. +func (c *Manifests) UpdateFromReader(r io.Reader) error { + // Bundle contents. + scanner := k8sutil.NewYAMLScanner(r) + for scanner.Scan() { + manifest := scanner.Bytes() + typeMeta, err := k8sutil.GetTypeMetaFromBytes(manifest) + if err != nil { + log.Debug("No TypeMeta found, skipping manifest") + continue } + + gvk := typeMeta.GroupVersionKind() + switch gvk.GroupKind() { + case roleGK: + err = c.addRoles(manifest) + case clusterRoleGK: + err = c.addClusterRoles(manifest) + case roleBindingGK: + err = c.addRoleBindings(manifest) + case clusterRoleBindingGK: + err = c.addClusterRoleBindings(manifest) + case serviceAccountGK: + err = c.addServiceAccounts(manifest) + case deploymentGK: + err = c.addDeployments(manifest) + case crdGK: + err = c.addCustomResourceDefinitions(gvk.Version, manifest) + case validatingWebhookCfgGK: + err = c.addValidatingWebhookConfigurations(manifest) + case mutatingWebhookCfgGK: + err = c.addMutatingWebhookConfigurations(manifest) + case v1alpha3ScorecardCfgGK: + err = c.addScorecardConfig(manifest) + default: + err = c.addOthers(manifest) + } + if err != nil { + return fmt.Errorf("error adding %s to manifest collector: %v", gvk, err) + } + } + if err := scanner.Err(); err != nil { + return fmt.Errorf("error collecting manifests from reader: %v", err) } - // Create a list of cluster role names to ignore. - deploymentRoleNames := make(map[string]struct{}) - for _, dep := range c.Deployments { - saName := dep.Spec.Template.Spec.ServiceAccountName - if saName == "" { - saName = defaultServiceAccountName + + // Filter manifests based on data collected. + c.filter() + + // Remove duplicate manifests. + if err := c.deduplicate(); err != nil { + return fmt.Errorf("error removing duplicate manifests: %v", err) + } + + return nil +} + +// addRoles assumes all manifest data in rawManifests are Roles and adds them +// to the collector. +func (c *Manifests) addRoles(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + role := rbacv1.Role{} + if err := yaml.Unmarshal(rawManifest, &role); err != nil { + return err } - for name := range saNamesToRoleNames[saName] { - deploymentRoleNames[name] = struct{}{} + c.Roles = append(c.Roles, role) + } + return nil +} + +// addClusterRoles assumes all manifest data in rawManifests are ClusterRoles +// and adds them to the collector. +func (c *Manifests) addClusterRoles(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + role := rbacv1.ClusterRole{} + if err := yaml.Unmarshal(rawManifest, &role); err != nil { + return err } + c.ClusterRoles = append(c.ClusterRoles, role) } - // Add all remaining cluster roles, which are not referenced in deployments (different service account or unbound). - for i, role := range c.Roles { - if _, hasName := deploymentRoleNames[role.GetName()]; !hasName { - objs = append(objs, &c.Roles[i]) + return nil +} + +// addRoleBindings assumes all manifest data in rawManifests are RoleBindings and adds them to the collector. +func (c *Manifests) addRoleBindings(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + binding := rbacv1.RoleBinding{} + if err := yaml.Unmarshal(rawManifest, &binding); err != nil { + return err } + c.RoleBindings = append(c.RoleBindings, binding) } + return nil +} - saNamesClusterToRoleNames := make(map[string]map[string]struct{}) - for _, binding := range c.ClusterRoleBindings { - roleRef := binding.RoleRef - if roleRef.Kind == "ClusterRole" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) { - for _, name := range getSubjectServiceAccountNames(binding.Subjects) { - if _, hasName := saNamesClusterToRoleNames[name]; !hasName { - saNamesClusterToRoleNames[name] = make(map[string]struct{}) - } - saNamesClusterToRoleNames[name][roleRef.Name] = struct{}{} - } +// addClusterRoleBindings assumes all manifest data in rawManifests are ClusterRoleBindings and adds them to the collector. +func (c *Manifests) addClusterRoleBindings(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + binding := rbacv1.ClusterRoleBinding{} + if err := yaml.Unmarshal(rawManifest, &binding); err != nil { + return err } + c.ClusterRoleBindings = append(c.ClusterRoleBindings, binding) } - // Create a list of cluster role names to ignore. - deploymentClusterRoleNames := make(map[string]struct{}) - for _, dep := range c.Deployments { - saName := dep.Spec.Template.Spec.ServiceAccountName - if saName == "" { - saName = "default" + return nil +} + +// addServiceAccounts assumes all manifest data in rawManifests are ServiceAccounts and adds them to the collector. +func (c *Manifests) addServiceAccounts(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + sa := corev1.ServiceAccount{} + if err := yaml.Unmarshal(rawManifest, &sa); err != nil { + return err + } + c.ServiceAccounts = append(c.ServiceAccounts, sa) + } + return nil +} + +// addDeployments assumes all manifest data in rawManifests are Deployments +// and adds them to the collector. +func (c *Manifests) addDeployments(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + dep := appsv1.Deployment{} + if err := yaml.Unmarshal(rawManifest, &dep); err != nil { + return err + } + c.Deployments = append(c.Deployments, dep) + } + return nil +} + +// addCustomResourceDefinitions assumes all manifest data in rawManifests are +// CustomResourceDefinitions and adds them to the collector. version determines +// which CustomResourceDefinition type is used for all manifests in rawManifests. +func (c *Manifests) addCustomResourceDefinitions(version string, rawManifests ...[]byte) (err error) { + for _, rawManifest := range rawManifests { + switch version { + case apiextv1.SchemeGroupVersion.Version: + crd := apiextv1.CustomResourceDefinition{} + if err := yaml.Unmarshal(rawManifest, &crd); err != nil { + return err + } + c.V1CustomResourceDefinitions = append(c.V1CustomResourceDefinitions, crd) + case apiextv1beta1.SchemeGroupVersion.Version: + crd := apiextv1beta1.CustomResourceDefinition{} + if err := yaml.Unmarshal(rawManifest, &crd); err != nil { + return err + } + c.V1beta1CustomResourceDefinitions = append(c.V1beta1CustomResourceDefinitions, crd) + default: + return fmt.Errorf("unrecognized CustomResourceDefinition version %q", version) } - for name := range saNamesClusterToRoleNames[saName] { - deploymentClusterRoleNames[name] = struct{}{} + } + return nil +} + +// addValidatingWebhookConfigurations assumes all manifest data in rawManifests +// are ValidatingWebhookConfigurations and adds their webhooks to the collector. +func (c *Manifests) addValidatingWebhookConfigurations(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + webhookConfig := admissionregv1.ValidatingWebhookConfiguration{} + if err := yaml.Unmarshal(rawManifest, &webhookConfig); err != nil { + return err } + c.ValidatingWebhooks = append(c.ValidatingWebhooks, webhookConfig.Webhooks...) } - // Add all remaining roles, which are not referenced in deployments (different service account or unbound). - for i, clusterRole := range c.ClusterRoles { - if _, hasName := deploymentClusterRoleNames[clusterRole.GetName()]; !hasName { - objs = append(objs, &c.ClusterRoles[i]) + return nil +} + +// addMutatingWebhookConfigurations assumes all manifest data in rawManifests +// are MutatingWebhookConfigurations and adds their webhooks to the collector. +func (c *Manifests) addMutatingWebhookConfigurations(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + webhookConfig := admissionregv1.MutatingWebhookConfiguration{} + if err := yaml.Unmarshal(rawManifest, &webhookConfig); err != nil { + return err } + c.MutatingWebhooks = append(c.MutatingWebhooks, webhookConfig.Webhooks...) } - return objs + return nil } -// getSubjectServiceAccountNames returns a list of all ServiceAccount subject names. -func getSubjectServiceAccountNames(subjects []rbacv1.Subject) (saNames []string) { - for _, subject := range subjects { - if subject.Kind == "ServiceAccount" { - saNames = append(saNames, subject.Name) +// addScorecardConfig assumes manifest data in rawManifests is a ScorecardConfigs and adds it to the collector. +// If a config has already been found, addScorecardConfig will return an error. +func (c *Manifests) addScorecardConfig(rawManifest []byte) error { + cfg := scorecardv1alpha3.Configuration{} + if err := yaml.Unmarshal(rawManifest, &cfg); err != nil { + return err + } + if c.ScorecardConfig.Metadata.Name != "" { + return errors.New("duplicate scorecard configurations in collector input") + } + c.ScorecardConfig = cfg + return nil +} + +// addOthers assumes all manifest data in rawManifests are able to be +// unmarshalled into an Unstructured object and adds them to the collector. +func (c *Manifests) addOthers(rawManifests ...[]byte) error { + for _, rawManifest := range rawManifests { + u := unstructured.Unstructured{} + if err := yaml.Unmarshal(rawManifest, &u); err != nil { + return err } + c.Others = append(c.Others, u) + } + return nil +} + +// isDirExist returns true if dir exists on disk. +func isDirExist(dir string) bool { + if dir == "" { + return false } - return saNames + info, err := os.Stat(dir) + return (err == nil && info.IsDir()) || os.IsExist(err) }