From 3e224b11c7662b6d331aa917d1b650b3e1a6575c Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Fri, 14 Feb 2025 12:00:23 +0100 Subject: [PATCH 1/5] Add installer rbac generator Signed-off-by: Per Goncalves da Silva --- .../rukpak/convert/installer_rbac.go | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 internal/operator-controller/rukpak/convert/installer_rbac.go diff --git a/internal/operator-controller/rukpak/convert/installer_rbac.go b/internal/operator-controller/rukpak/convert/installer_rbac.go new file mode 100644 index 000000000..293fc3a4a --- /dev/null +++ b/internal/operator-controller/rukpak/convert/installer_rbac.go @@ -0,0 +1,223 @@ +package convert + +import ( + "fmt" + "github.com/operator-framework/operator-controller/internal/shared/util/filter" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "slices" + "strings" +) + +var ( + unnamedResourceVerbs = []string{"create", "list", "watch"} + namedResourceVerbs = []string{"get", "update", "patch", "delete"} + + // clusterScopedResources is a slice of registry+v1 bundle supported cluster scoped resource kinds + clusterScopedResources = []string{ + "ClusterRole", + "ClusterRoleBinding", + "PriorityClass", + "ConsoleYAMLSample", + "ConsoleQuickStart", + "ConsoleCLIDownload", + "ConsoleLink", + "CustomResourceDefinition", + } + + // clusterScopedResources is a slice of registry+v1 bundle supported namespace scoped resource kinds + namespaceScopedResources = []string{ + "Secret", + "ConfigMap", + "ServiceAccount", + "Service", + "Role", + "RoleBinding", + "PrometheusRule", + "ServiceMonitor", + "PodDisruptionBudget", + "VerticalPodAutoscaler", + "Deployment", + } +) + +func GenerateInstallerRBAC(objs []client.Object, extensionName string, installNamespace string, watchNamespace string) []client.Object { + generatedObjs := getGeneratedObjs(objs) + + var ( + serviceAccountName = extensionName + "-installer" + clusterRoleName = extensionName + "-installer-clusterrole" + clusterRoleBindingName = extensionName + "-installer-clusterrole-binding" + roleName = extensionName + "-installer-role" + roleBindingName = extensionName + "-installer-role-binding" + ) + + rbacManifests := []client.Object{ + // installer service account + ptr.To(newServiceAccount( + installNamespace, + serviceAccountName, + )), + + // cluster scoped resources + ptr.To(newClusterRole( + clusterRoleName, + slices.Concat( + // finalizer rule + []rbacv1.PolicyRule{newClusterExtensionFinalizerPolicyRule(extensionName)}, + // cluster scoped resource creation and management rules + generatePolicyRules(filter.Filter(objs, isClusterScopedResource)), + // controller rbac scope + collectRBACResourcePolicyRules(filter.Filter(generatedObjs, isClusterRole)), + ), + )), + ptr.To(newClusterRoleBinding( + clusterRoleBindingName, + clusterRoleName, + installNamespace, + serviceAccountName, + )), + + // namespace scoped install namespace resources + ptr.To(newRole( + installNamespace, + roleName, + slices.Concat( + // namespace scoped resource creation and management rules + generatePolicyRules(filter.Filter(objs, filter.And(isNamespacedResource, inNamespace(installNamespace)))), + // controller rbac scope + collectRBACResourcePolicyRules(filter.Filter(generatedObjs, filter.And(isRole, inNamespace(installNamespace)))), + ), + )), + ptr.To(newRoleBinding( + installNamespace, + roleBindingName, + roleName, + installNamespace, + serviceAccountName, + )), + + // namespace scoped watch namespace resources + ptr.To(newRole( + watchNamespace, + roleName, + slices.Concat( + // namespace scoped resource creation and management rules + generatePolicyRules(filter.Filter(objs, filter.And(isNamespacedResource, inNamespace(watchNamespace)))), + // controller rbac scope + collectRBACResourcePolicyRules(filter.Filter(generatedObjs, filter.And(isRole, inNamespace(watchNamespace)))), + ), + )), + ptr.To(newRoleBinding( + watchNamespace, + roleBindingName, + roleName, + installNamespace, + serviceAccountName, + )), + } + + // remove any cluster/role(s) without any defined rules and pair cluster/role add manifests + return slices.DeleteFunc(rbacManifests, isNoRules) +} + +func isNoRules(object client.Object) bool { + switch obj := object.(type) { + case *rbacv1.ClusterRole: + return len(obj.Rules) == 0 + case *rbacv1.Role: + return len(obj.Rules) == 0 + } + return false +} + +func getGeneratedObjs(plainObjs []client.Object) []client.Object { + return filter.Filter(plainObjs, func(obj client.Object) bool { + // this is a hack that abuses an internal implementation detail + // we should probably annotate the generated resources coming out of convert.Convert + _, ok := obj.(*unstructured.Unstructured) + return ok + }) +} + +var isNamespacedResource filter.Predicate[client.Object] = func(o client.Object) bool { + return slices.Contains(namespaceScopedResources, o.GetObjectKind().GroupVersionKind().Kind) +} + +var isClusterScopedResource filter.Predicate[client.Object] = func(o client.Object) bool { + return slices.Contains(clusterScopedResources, o.GetObjectKind().GroupVersionKind().Kind) +} + +var isClusterRole = isOfKind("ClusterRole") +var isRole = isOfKind("Role") + +func isOfKind(kind string) filter.Predicate[client.Object] { + return func(o client.Object) bool { + return o.GetObjectKind().GroupVersionKind().Kind == kind + } +} + +func inNamespace(namespace string) filter.Predicate[client.Object] { + return func(o client.Object) bool { + return o.GetNamespace() == namespace + } +} + +func generatePolicyRules(objs []client.Object) []rbacv1.PolicyRule { + resourceNameMap := groupResourceNamesByGroupKind(objs) + policyRules := make([]rbacv1.PolicyRule, 0, 2*len(resourceNameMap)) + for groupKind, resourceNames := range resourceNameMap { + policyRules = append(policyRules, []rbacv1.PolicyRule{ + newPolicyRule(groupKind, unnamedResourceVerbs), + newPolicyRule(groupKind, namedResourceVerbs, resourceNames...), + }...) + } + return policyRules +} + +func collectRBACResourcePolicyRules(objs []client.Object) []rbacv1.PolicyRule { + var policyRules []rbacv1.PolicyRule + for _, obj := range objs { + if cr, ok := obj.(*rbacv1.ClusterRole); ok { + policyRules = append(policyRules, cr.Rules...) + } else if r, ok := obj.(*rbacv1.Role); ok { + policyRules = append(policyRules, r.Rules...) + } else { + panic(fmt.Sprintf("unexpected type %T", obj)) + } + } + return policyRules +} + +func newClusterExtensionFinalizerPolicyRule(clusterExtensionName string) rbacv1.PolicyRule { + return rbacv1.PolicyRule{ + APIGroups: []string{"olm.operatorframework.io"}, + Resources: []string{"clusterextensions/finalizers"}, + Verbs: []string{"update"}, + ResourceNames: []string{clusterExtensionName}, + } +} + +func groupResourceNamesByGroupKind(objs []client.Object) map[schema.GroupKind][]string { + resourceNames := map[schema.GroupKind][]string{} + for _, obj := range objs { + key := obj.GetObjectKind().GroupVersionKind().GroupKind() + if _, ok := resourceNames[key]; !ok { + resourceNames[key] = []string{} + } + resourceNames[key] = append(resourceNames[key], obj.GetName()) + } + return resourceNames +} + +func newPolicyRule(groupKind schema.GroupKind, verbs []string, resourceNames ...string) rbacv1.PolicyRule { + return rbacv1.PolicyRule{ + APIGroups: []string{groupKind.Group}, + Resources: []string{fmt.Sprintf("%ss", strings.ToLower(groupKind.Kind))}, + Verbs: verbs, + ResourceNames: resourceNames, + } +} From 93c409b79f55d80a0b7121acc6f2967aa38fcdf6 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Mon, 24 Feb 2025 09:38:30 +0100 Subject: [PATCH 2/5] Refactor Signed-off-by: Per Goncalves da Silva --- .../rukpak/convert/installer_rbac.go | 232 +++++++----------- internal/shared/util/filter/filter.go | 21 ++ 2 files changed, 105 insertions(+), 148 deletions(-) diff --git a/internal/operator-controller/rukpak/convert/installer_rbac.go b/internal/operator-controller/rukpak/convert/installer_rbac.go index 293fc3a4a..65ade1c70 100644 --- a/internal/operator-controller/rukpak/convert/installer_rbac.go +++ b/internal/operator-controller/rukpak/convert/installer_rbac.go @@ -44,180 +44,116 @@ var ( } ) -func GenerateInstallerRBAC(objs []client.Object, extensionName string, installNamespace string, watchNamespace string) []client.Object { - generatedObjs := getGeneratedObjs(objs) - - var ( - serviceAccountName = extensionName + "-installer" - clusterRoleName = extensionName + "-installer-clusterrole" - clusterRoleBindingName = extensionName + "-installer-clusterrole-binding" - roleName = extensionName + "-installer-role" - roleBindingName = extensionName + "-installer-role-binding" - ) - - rbacManifests := []client.Object{ - // installer service account - ptr.To(newServiceAccount( - installNamespace, - serviceAccountName, - )), - - // cluster scoped resources - ptr.To(newClusterRole( - clusterRoleName, - slices.Concat( - // finalizer rule - []rbacv1.PolicyRule{newClusterExtensionFinalizerPolicyRule(extensionName)}, - // cluster scoped resource creation and management rules - generatePolicyRules(filter.Filter(objs, isClusterScopedResource)), - // controller rbac scope - collectRBACResourcePolicyRules(filter.Filter(generatedObjs, isClusterRole)), - ), - )), - ptr.To(newClusterRoleBinding( - clusterRoleBindingName, - clusterRoleName, - installNamespace, - serviceAccountName, - )), - - // namespace scoped install namespace resources - ptr.To(newRole( - installNamespace, - roleName, - slices.Concat( - // namespace scoped resource creation and management rules - generatePolicyRules(filter.Filter(objs, filter.And(isNamespacedResource, inNamespace(installNamespace)))), - // controller rbac scope - collectRBACResourcePolicyRules(filter.Filter(generatedObjs, filter.And(isRole, inNamespace(installNamespace)))), - ), - )), - ptr.To(newRoleBinding( - installNamespace, - roleBindingName, - roleName, - installNamespace, - serviceAccountName, - )), - - // namespace scoped watch namespace resources - ptr.To(newRole( - watchNamespace, - roleName, - slices.Concat( - // namespace scoped resource creation and management rules - generatePolicyRules(filter.Filter(objs, filter.And(isNamespacedResource, inNamespace(watchNamespace)))), - // controller rbac scope - collectRBACResourcePolicyRules(filter.Filter(generatedObjs, filter.And(isRole, inNamespace(watchNamespace)))), - ), - )), - ptr.To(newRoleBinding( - watchNamespace, - roleBindingName, - roleName, - installNamespace, - serviceAccountName, - )), - } - - // remove any cluster/role(s) without any defined rules and pair cluster/role add manifests - return slices.DeleteFunc(rbacManifests, isNoRules) +// GenerateResourceManagerClusterRole generates a ClusterRole with permissions to manage objs resources. The +// permissions also aggregate any permissions from any ClusterRoles in objs allowing the holder to also assign +// the RBAC therein to another service account. Note: currently assumes objs have been created by convert.Convert. +func GenerateResourceManagerClusterRole(objs []client.Object) *rbacv1.ClusterRole { + return ptr.To(newClusterRole( + "", + slices.Concat( + // cluster scoped resource creation and management rules + generatePolicyRules(filter.Filter(objs, isClusterScopedResource)), + // controller rbac scope + collectRBACResourcePolicyRules(filter.Filter(objs, filter.And(isGeneratedResource, isOfKind("ClusterRole")))), + ), + )) } -func isNoRules(object client.Object) bool { - switch obj := object.(type) { - case *rbacv1.ClusterRole: - return len(obj.Rules) == 0 - case *rbacv1.Role: - return len(obj.Rules) == 0 +// GenerateClusterExtensionFinalizerPolicyRule generates a policy rule that allows the holder to update +// finalizer for a ClusterExtension with clusterExtensionName. +func GenerateClusterExtensionFinalizerPolicyRule(clusterExtensionName string) rbacv1.PolicyRule { + return rbacv1.PolicyRule{ + APIGroups: []string{"olm.operatorframework.io"}, + Resources: []string{"clusterextensions/finalizers"}, + Verbs: []string{"update"}, + ResourceNames: []string{clusterExtensionName}, } - return false } -func getGeneratedObjs(plainObjs []client.Object) []client.Object { - return filter.Filter(plainObjs, func(obj client.Object) bool { - // this is a hack that abuses an internal implementation detail - // we should probably annotate the generated resources coming out of convert.Convert - _, ok := obj.(*unstructured.Unstructured) - return ok - }) +// GenerateResourceManagerRoles generates one or more Roles with permissions to manage objs resources in their +// namespaces. The permissions also include any permissions defined in any Roles in objs within the namespace, allowing +// the holder to also assign the RBAC therein to another service account. +// Note: currently assumes objs have been created by convert.Convert. +func GenerateResourceManagerRoles(objs []client.Object) []*rbacv1.Role { + return mapToSlice(filter.GroupBy(objs, namespaceName), generateRole) } -var isNamespacedResource filter.Predicate[client.Object] = func(o client.Object) bool { - return slices.Contains(namespaceScopedResources, o.GetObjectKind().GroupVersionKind().Kind) -} - -var isClusterScopedResource filter.Predicate[client.Object] = func(o client.Object) bool { - return slices.Contains(clusterScopedResources, o.GetObjectKind().GroupVersionKind().Kind) -} - -var isClusterRole = isOfKind("ClusterRole") -var isRole = isOfKind("Role") - -func isOfKind(kind string) filter.Predicate[client.Object] { - return func(o client.Object) bool { - return o.GetObjectKind().GroupVersionKind().Kind == kind - } -} - -func inNamespace(namespace string) filter.Predicate[client.Object] { - return func(o client.Object) bool { - return o.GetNamespace() == namespace - } +func generateRole(namespace string, namespaceObjs []client.Object) *rbacv1.Role { + return ptr.To(newRole( + namespace, + "", + slices.Concat( + // namespace scoped resource creation and management rules + generatePolicyRules(namespaceObjs), + // controller rbac scope + collectRBACResourcePolicyRules(filter.Filter(namespaceObjs, filter.And(isOfKind("Role"), isGeneratedResource))), + ), + )) } func generatePolicyRules(objs []client.Object) []rbacv1.PolicyRule { - resourceNameMap := groupResourceNamesByGroupKind(objs) - policyRules := make([]rbacv1.PolicyRule, 0, 2*len(resourceNameMap)) - for groupKind, resourceNames := range resourceNameMap { - policyRules = append(policyRules, []rbacv1.PolicyRule{ - newPolicyRule(groupKind, unnamedResourceVerbs), - newPolicyRule(groupKind, namedResourceVerbs, resourceNames...), - }...) - } - return policyRules + return slices.Concat(mapToSlice(filter.GroupBy(objs, groupKind), func(gk schema.GroupKind, resources []client.Object) []rbacv1.PolicyRule { + return []rbacv1.PolicyRule{ + newPolicyRule(gk, unnamedResourceVerbs), + newPolicyRule(gk, namedResourceVerbs, filter.Map(resources, toResourceName)...), + } + })...) } func collectRBACResourcePolicyRules(objs []client.Object) []rbacv1.PolicyRule { - var policyRules []rbacv1.PolicyRule - for _, obj := range objs { + return slices.Concat(filter.Map(objs, func(obj client.Object) []rbacv1.PolicyRule { if cr, ok := obj.(*rbacv1.ClusterRole); ok { - policyRules = append(policyRules, cr.Rules...) + return cr.Rules } else if r, ok := obj.(*rbacv1.Role); ok { - policyRules = append(policyRules, r.Rules...) + return r.Rules } else { panic(fmt.Sprintf("unexpected type %T", obj)) } - } - return policyRules + })...) } -func newClusterExtensionFinalizerPolicyRule(clusterExtensionName string) rbacv1.PolicyRule { +func newPolicyRule(groupKind schema.GroupKind, verbs []string, resourceNames ...string) rbacv1.PolicyRule { return rbacv1.PolicyRule{ - APIGroups: []string{"olm.operatorframework.io"}, - Resources: []string{"clusterextensions/finalizers"}, - Verbs: []string{"update"}, - ResourceNames: []string{clusterExtensionName}, + APIGroups: []string{groupKind.Group}, + Resources: []string{fmt.Sprintf("%ss", strings.ToLower(groupKind.Kind))}, + Verbs: verbs, + ResourceNames: resourceNames, } } -func groupResourceNamesByGroupKind(objs []client.Object) map[schema.GroupKind][]string { - resourceNames := map[schema.GroupKind][]string{} - for _, obj := range objs { - key := obj.GetObjectKind().GroupVersionKind().GroupKind() - if _, ok := resourceNames[key]; !ok { - resourceNames[key] = []string{} - } - resourceNames[key] = append(resourceNames[key], obj.GetName()) +func mapToSlice[K comparable, V any, R any](m map[K]V, fn func(k K, v V) R) []R { + out := make([]R, 0, len(m)) + for k, v := range m { + out = append(out, fn(k, v)) } - return resourceNames + return out } -func newPolicyRule(groupKind schema.GroupKind, verbs []string, resourceNames ...string) rbacv1.PolicyRule { - return rbacv1.PolicyRule{ - APIGroups: []string{groupKind.Group}, - Resources: []string{fmt.Sprintf("%ss", strings.ToLower(groupKind.Kind))}, - Verbs: verbs, - ResourceNames: resourceNames, +func isClusterScopedResource(o client.Object) bool { + return slices.Contains(clusterScopedResources, o.GetObjectKind().GroupVersionKind().Kind) +} + +func isOfKind(kind string) filter.Predicate[client.Object] { + return func(o client.Object) bool { + return o.GetObjectKind().GroupVersionKind().Kind == kind } } + +func isGeneratedResource(o client.Object) bool { + // TODO: this is a hack that abuses an internal implementation detail + // we should probably annotate the generated resources coming out of convert.Convert + _, ok := o.(*unstructured.Unstructured) + return ok +} + +func groupKind(obj client.Object) schema.GroupKind { + return obj.GetObjectKind().GroupVersionKind().GroupKind() +} + +func namespaceName(obj client.Object) string { + return obj.GetNamespace() +} + +func toResourceName(o client.Object) string { + return o.GetName() +} diff --git a/internal/shared/util/filter/filter.go b/internal/shared/util/filter/filter.go index 96d60cfcd..885816283 100644 --- a/internal/shared/util/filter/filter.go +++ b/internal/shared/util/filter/filter.go @@ -5,6 +5,10 @@ import "slices" // Predicate returns true if the object should be kept when filtering type Predicate[T any] func(entity T) bool +type Key[T any, K comparable] func(entity T) K + +type MapFn[S any, V any] func(S) V + func And[T any](predicates ...Predicate[T]) Predicate[T] { return func(obj T) bool { for _, predicate := range predicates { @@ -50,3 +54,20 @@ func Filter[T any](s []T, test Predicate[T]) []T { func InPlace[T any](s []T, test Predicate[T]) []T { return slices.DeleteFunc(s, Not(test)) } + +func GroupBy[T any, K comparable](s []T, key Key[T, K]) map[K][]T { + out := map[K][]T{} + for _, value := range s { + k := key(value) + out[k] = append(out[k], value) + } + return out +} + +func Map[S, V any](s []S, mapper MapFn[S, V]) []V { + out := make([]V, len(s)) + for i := 0; i < len(s); i++ { + out[i] = mapper(s[i]) + } + return out +} From 5fd0d76211f34d7dd3c0f1b5f91fae1435c99168 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 4 Mar 2025 14:01:45 +0100 Subject: [PATCH 3/5] Annotated generated manifests Signed-off-by: Per Goncalves da Silva --- .../rukpak/convert/installer_rbac.go | 215 +++++++++--------- .../rukpak/convert/registryv1.go | 28 ++- .../rukpak/convert/registryv1_test.go | 43 +++- 3 files changed, 176 insertions(+), 110 deletions(-) diff --git a/internal/operator-controller/rukpak/convert/installer_rbac.go b/internal/operator-controller/rukpak/convert/installer_rbac.go index 65ade1c70..77ac8b3f1 100644 --- a/internal/operator-controller/rukpak/convert/installer_rbac.go +++ b/internal/operator-controller/rukpak/convert/installer_rbac.go @@ -1,159 +1,168 @@ package convert import ( - "fmt" - "github.com/operator-framework/operator-controller/internal/shared/util/filter" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - "slices" - "strings" + "fmt" + "slices" + "strings" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/filter" ) var ( - unnamedResourceVerbs = []string{"create", "list", "watch"} - namedResourceVerbs = []string{"get", "update", "patch", "delete"} - - // clusterScopedResources is a slice of registry+v1 bundle supported cluster scoped resource kinds - clusterScopedResources = []string{ - "ClusterRole", - "ClusterRoleBinding", - "PriorityClass", - "ConsoleYAMLSample", - "ConsoleQuickStart", - "ConsoleCLIDownload", - "ConsoleLink", - "CustomResourceDefinition", - } - - // clusterScopedResources is a slice of registry+v1 bundle supported namespace scoped resource kinds - namespaceScopedResources = []string{ - "Secret", - "ConfigMap", - "ServiceAccount", - "Service", - "Role", - "RoleBinding", - "PrometheusRule", - "ServiceMonitor", - "PodDisruptionBudget", - "VerticalPodAutoscaler", - "Deployment", - } + unnamedResourceVerbs = []string{"create", "list", "watch"} + namedResourceVerbs = []string{"get", "update", "patch", "delete"} + + // clusterScopedResources is a slice of registry+v1 bundle supported cluster scoped resource kinds + clusterScopedResources = []string{ + "ClusterRole", + "ClusterRoleBinding", + "PriorityClass", + "ConsoleYAMLSample", + "ConsoleQuickStart", + "ConsoleCLIDownload", + "ConsoleLink", + "CustomResourceDefinition", + } + + // clusterScopedResources is a slice of registry+v1 bundle supported namespace scoped resource kinds + namespaceScopedResources = []string{ + "Secret", + "ConfigMap", + "ServiceAccount", + "Service", + "Role", + "RoleBinding", + "PrometheusRule", + "ServiceMonitor", + "PodDisruptionBudget", + "VerticalPodAutoscaler", + "Deployment", + } ) // GenerateResourceManagerClusterRole generates a ClusterRole with permissions to manage objs resources. The // permissions also aggregate any permissions from any ClusterRoles in objs allowing the holder to also assign -// the RBAC therein to another service account. Note: currently assumes objs have been created by convert.Convert. +// the RBAC therein to another service account. Note: assumes objs have been created by convert.Convert. +// The returned ClusterRole will not have set .metadata.name func GenerateResourceManagerClusterRole(objs []client.Object) *rbacv1.ClusterRole { - return ptr.To(newClusterRole( - "", - slices.Concat( - // cluster scoped resource creation and management rules - generatePolicyRules(filter.Filter(objs, isClusterScopedResource)), - // controller rbac scope - collectRBACResourcePolicyRules(filter.Filter(objs, filter.And(isGeneratedResource, isOfKind("ClusterRole")))), - ), - )) + rules := slices.Concat( + // cluster scoped resource creation and management rules + generatePolicyRules(slicesutil.Filter(objs, isClusterScopedResource)), + // controller rbac scope + collectRBACResourcePolicyRules(slicesutil.Filter(objs, slicesutil.And(isGeneratedResource, isOfKind("ClusterRole")))), + ) + if len(rules) == 0 { + return nil + } + return ptr.To(newClusterRole("", rules)) } // GenerateClusterExtensionFinalizerPolicyRule generates a policy rule that allows the holder to update // finalizer for a ClusterExtension with clusterExtensionName. func GenerateClusterExtensionFinalizerPolicyRule(clusterExtensionName string) rbacv1.PolicyRule { - return rbacv1.PolicyRule{ - APIGroups: []string{"olm.operatorframework.io"}, - Resources: []string{"clusterextensions/finalizers"}, - Verbs: []string{"update"}, - ResourceNames: []string{clusterExtensionName}, - } + return rbacv1.PolicyRule{ + APIGroups: []string{"olm.operatorframework.io"}, + Resources: []string{"clusterextensions/finalizers"}, + Verbs: []string{"update"}, + ResourceNames: []string{clusterExtensionName}, + } } // GenerateResourceManagerRoles generates one or more Roles with permissions to manage objs resources in their // namespaces. The permissions also include any permissions defined in any Roles in objs within the namespace, allowing // the holder to also assign the RBAC therein to another service account. // Note: currently assumes objs have been created by convert.Convert. +// The returned Roles will not have set .metadata.name func GenerateResourceManagerRoles(objs []client.Object) []*rbacv1.Role { - return mapToSlice(filter.GroupBy(objs, namespaceName), generateRole) + return mapToSlice(slicesutil.GroupBy(slicesutil.Filter(objs, isNamespaceScopedResource), namespaceName), generateRole) } func generateRole(namespace string, namespaceObjs []client.Object) *rbacv1.Role { - return ptr.To(newRole( - namespace, - "", - slices.Concat( - // namespace scoped resource creation and management rules - generatePolicyRules(namespaceObjs), - // controller rbac scope - collectRBACResourcePolicyRules(filter.Filter(namespaceObjs, filter.And(isOfKind("Role"), isGeneratedResource))), - ), - )) + return ptr.To(newRole( + namespace, + "", + slices.Concat( + // namespace scoped resource creation and management rules + generatePolicyRules(namespaceObjs), + // controller rbac scope + collectRBACResourcePolicyRules(slicesutil.Filter(namespaceObjs, slicesutil.And(isOfKind("Role"), isGeneratedResource))), + ), + )) } func generatePolicyRules(objs []client.Object) []rbacv1.PolicyRule { - return slices.Concat(mapToSlice(filter.GroupBy(objs, groupKind), func(gk schema.GroupKind, resources []client.Object) []rbacv1.PolicyRule { - return []rbacv1.PolicyRule{ - newPolicyRule(gk, unnamedResourceVerbs), - newPolicyRule(gk, namedResourceVerbs, filter.Map(resources, toResourceName)...), - } - })...) + return slices.Concat( + mapToSlice(slicesutil.GroupBy(objs, groupKind), func(gk schema.GroupKind, resources []client.Object) []rbacv1.PolicyRule { + return []rbacv1.PolicyRule{ + newPolicyRule(gk, unnamedResourceVerbs), + newPolicyRule(gk, namedResourceVerbs, slicesutil.Map(resources, toResourceName)...), + } + })..., + ) } func collectRBACResourcePolicyRules(objs []client.Object) []rbacv1.PolicyRule { - return slices.Concat(filter.Map(objs, func(obj client.Object) []rbacv1.PolicyRule { - if cr, ok := obj.(*rbacv1.ClusterRole); ok { - return cr.Rules - } else if r, ok := obj.(*rbacv1.Role); ok { - return r.Rules - } else { - panic(fmt.Sprintf("unexpected type %T", obj)) - } - })...) + return slices.Concat(slicesutil.Map(objs, func(obj client.Object) []rbacv1.PolicyRule { + if cr, ok := obj.(*rbacv1.ClusterRole); ok { + return cr.Rules + } else if r, ok := obj.(*rbacv1.Role); ok { + return r.Rules + } else { + panic(fmt.Sprintf("unexpected type %T", obj)) + } + })...) } func newPolicyRule(groupKind schema.GroupKind, verbs []string, resourceNames ...string) rbacv1.PolicyRule { - return rbacv1.PolicyRule{ - APIGroups: []string{groupKind.Group}, - Resources: []string{fmt.Sprintf("%ss", strings.ToLower(groupKind.Kind))}, - Verbs: verbs, - ResourceNames: resourceNames, - } + return rbacv1.PolicyRule{ + APIGroups: []string{groupKind.Group}, + Resources: []string{fmt.Sprintf("%ss", strings.ToLower(groupKind.Kind))}, + Verbs: verbs, + ResourceNames: resourceNames, + } } func mapToSlice[K comparable, V any, R any](m map[K]V, fn func(k K, v V) R) []R { - out := make([]R, 0, len(m)) - for k, v := range m { - out = append(out, fn(k, v)) - } - return out + out := make([]R, 0, len(m)) + for k, v := range m { + out = append(out, fn(k, v)) + } + return out } func isClusterScopedResource(o client.Object) bool { - return slices.Contains(clusterScopedResources, o.GetObjectKind().GroupVersionKind().Kind) + return slices.Contains(clusterScopedResources, o.GetObjectKind().GroupVersionKind().Kind) +} + +func isNamespaceScopedResource(o client.Object) bool { + return slices.Contains(namespaceScopedResources, o.GetObjectKind().GroupVersionKind().Kind) } -func isOfKind(kind string) filter.Predicate[client.Object] { - return func(o client.Object) bool { - return o.GetObjectKind().GroupVersionKind().Kind == kind - } +func isOfKind(kind string) slicesutil.Predicate[client.Object] { + return func(o client.Object) bool { + return o.GetObjectKind().GroupVersionKind().Kind == kind + } } func isGeneratedResource(o client.Object) bool { - // TODO: this is a hack that abuses an internal implementation detail - // we should probably annotate the generated resources coming out of convert.Convert - _, ok := o.(*unstructured.Unstructured) - return ok + annotations := o.GetAnnotations() + _, ok := annotations[AnnotationRegistryV1GeneratedManifest] + return ok } func groupKind(obj client.Object) schema.GroupKind { - return obj.GetObjectKind().GroupVersionKind().GroupKind() + return obj.GetObjectKind().GroupVersionKind().GroupKind() } func namespaceName(obj client.Object) string { - return obj.GetNamespace() + return obj.GetNamespace() } func toResourceName(o client.Object) string { - return o.GetName() + return o.GetName() } diff --git a/internal/operator-controller/rukpak/convert/registryv1.go b/internal/operator-controller/rukpak/convert/registryv1.go index 155b86be8..8f44eb375 100644 --- a/internal/operator-controller/rukpak/convert/registryv1.go +++ b/internal/operator-controller/rukpak/convert/registryv1.go @@ -29,6 +29,10 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" ) +const ( + AnnotationRegistryV1GeneratedManifest = "io.operatorframework.olm.generated-manifest" +) + type RegistryV1 struct { PackageName string CSV v1alpha1.ClusterServiceVersion @@ -255,7 +259,7 @@ func Convert(in RegistryV1, installNamespace string, targetNamespaces []string) return nil, fmt.Errorf("webhookDefinitions are not supported") } - deployments := []appsv1.Deployment{} + deployments := make([]appsv1.Deployment, 0, len(in.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs)) serviceAccounts := map[string]corev1.ServiceAccount{} for _, depSpec := range in.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs { annotations := util.MergeMaps(in.CSV.Annotations, depSpec.Spec.Template.Annotations) @@ -340,23 +344,24 @@ func Convert(in RegistryV1, installNamespace string, targetNamespaces []string) } objs := []client.Object{} + for _, obj := range serviceAccounts { obj := obj if obj.GetName() != "default" { - objs = append(objs, &obj) + objs = append(objs, annotateGenerated(&obj)) } } for _, obj := range roles { obj := obj - objs = append(objs, &obj) + objs = append(objs, annotateGenerated(&obj)) } for _, obj := range roleBindings { obj := obj - objs = append(objs, &obj) + objs = append(objs, annotateGenerated(&obj)) } for _, obj := range clusterRoles { obj := obj - objs = append(objs, &obj) + objs = append(objs, annotateGenerated(&obj)) } for _, obj := range clusterRoleBindings { obj := obj @@ -375,13 +380,24 @@ func Convert(in RegistryV1, installNamespace string, targetNamespaces []string) } for _, obj := range deployments { obj := obj - objs = append(objs, &obj) + objs = append(objs, annotateGenerated(&obj)) } + return &Plain{Objects: objs}, nil } const maxNameLength = 63 +func annotateGenerated(o client.Object) client.Object { + annotations := o.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[AnnotationRegistryV1GeneratedManifest] = "" + o.SetAnnotations(annotations) + return o +} + func generateName(base string, o interface{}) (string, error) { hashStr, err := util.DeepHashObject(o) if err != nil { diff --git a/internal/operator-controller/rukpak/convert/registryv1_test.go b/internal/operator-controller/rukpak/convert/registryv1_test.go index 20ea7fc88..286a035da 100644 --- a/internal/operator-controller/rukpak/convert/registryv1_test.go +++ b/internal/operator-controller/rukpak/convert/registryv1_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io/fs" "os" + "reflect" "strings" "testing" "testing/fstest" @@ -346,6 +347,46 @@ func TestRegistryV1SuiteGenerateSingleNamespace(t *testing.T) { require.Equal(t, strings.Join(watchNamespaces, ","), dep.(*appsv1.Deployment).Spec.Template.Annotations[olmNamespaces]) } +func TestRegistryV1SuiteConvertAnnotatesGeneratedManifests(t *testing.T) { + t.Log("RegistryV1 Suite Convert") + t.Log("It should generate objects successfully based on target namespaces") + + t.Log("It should convert into plain manifests successfully with SingleNamespace") + baseCSV, svc := getBaseCsvAndService() + csv := baseCSV.DeepCopy() + csv.Spec.InstallModes = []v1alpha1.InstallMode{{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}} + + t.Log("By creating a registry v1 bundle") + watchNamespaces := []string{"testWatchNs1"} + unstructuredSvc := convertToUnstructured(t, svc) + registryv1Bundle := convert.RegistryV1{ + PackageName: "testPkg", + CSV: *csv, + Others: []unstructured.Unstructured{unstructuredSvc}, + } + + t.Log("By converting to plain") + plainBundle, err := convert.Convert(registryv1Bundle, installNamespace, watchNamespaces) + require.NoError(t, err) + + t.Log("By verifying if plain bundle has required objects") + require.NotNil(t, plainBundle) + require.Len(t, plainBundle.Objects, 5) + + t.Log("By verifying all manifests not in 'Others' contain 'io.operatorframework.olm.generated-manifest' annotation") + for _, obj := range plainBundle.Objects { + if reflect.DeepEqual(obj, &unstructuredSvc) { + require.NotContains(t, obj.GetAnnotations(), convert.AnnotationRegistryV1GeneratedManifest) + } else { + require.Contains(t, obj.GetAnnotations(), convert.AnnotationRegistryV1GeneratedManifest) + } + } + dep := findObjectByName("testDeployment", plainBundle.Objects) + require.NotNil(t, dep) + require.Contains(t, dep.(*appsv1.Deployment).Spec.Template.Annotations, olmNamespaces) + require.Equal(t, strings.Join(watchNamespaces, ","), dep.(*appsv1.Deployment).Spec.Template.Annotations[olmNamespaces]) +} + func TestRegistryV1SuiteGenerateOwnNamespace(t *testing.T) { t.Log("RegistryV1 Suite Convert") t.Log("It should generate objects successfully based on target namespaces") @@ -565,7 +606,7 @@ func TestRegistryV1SuiteGenerateNoWebhooks(t *testing.T) { require.Nil(t, plainBundle) } -func TestRegistryV1SuiteGenerateNoAPISerciceDefinitions(t *testing.T) { +func TestRegistryV1SuiteGenerateNoAPIServiceDefinitions(t *testing.T) { t.Log("RegistryV1 Suite Convert") t.Log("It should generate objects successfully based on target namespaces") From bcd8b5b3d5a14ab0178a5375cd4ee7ce94e70c1d Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 4 Mar 2025 15:37:21 +0100 Subject: [PATCH 4/5] Move Map and GroupBy to slice package Signed-off-by: Per Goncalves da Silva --- .../rukpak/convert/installer_rbac.go | 64 +++++++++---------- internal/shared/util/filter/filter.go | 21 ------ internal/shared/util/slices/slices.go | 22 +++++++ internal/shared/util/slices/slices_test.go | 31 +++++++++ 4 files changed, 84 insertions(+), 54 deletions(-) create mode 100644 internal/shared/util/slices/slices.go create mode 100644 internal/shared/util/slices/slices_test.go diff --git a/internal/operator-controller/rukpak/convert/installer_rbac.go b/internal/operator-controller/rukpak/convert/installer_rbac.go index 77ac8b3f1..76612d3a2 100644 --- a/internal/operator-controller/rukpak/convert/installer_rbac.go +++ b/internal/operator-controller/rukpak/convert/installer_rbac.go @@ -7,10 +7,10 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" - slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/filter" + "github.com/operator-framework/operator-controller/internal/shared/util/filter" + slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/slices" ) var ( @@ -45,21 +45,16 @@ var ( } ) -// GenerateResourceManagerClusterRole generates a ClusterRole with permissions to manage objs resources. The +// GenerateResourceManagerClusterRolePerms generates a ClusterRole permissions to manage objs resources. The // permissions also aggregate any permissions from any ClusterRoles in objs allowing the holder to also assign // the RBAC therein to another service account. Note: assumes objs have been created by convert.Convert. -// The returned ClusterRole will not have set .metadata.name -func GenerateResourceManagerClusterRole(objs []client.Object) *rbacv1.ClusterRole { - rules := slices.Concat( +func GenerateResourceManagerClusterRolePerms(objs []client.Object) []rbacv1.PolicyRule { + return slices.Concat( // cluster scoped resource creation and management rules - generatePolicyRules(slicesutil.Filter(objs, isClusterScopedResource)), + generatePolicyRules(filter.Filter(objs, isClusterScopedResource)), // controller rbac scope - collectRBACResourcePolicyRules(slicesutil.Filter(objs, slicesutil.And(isGeneratedResource, isOfKind("ClusterRole")))), + collectRBACResourcePolicyRules(filter.Filter(objs, filter.And(isGeneratedResource, isOfKind("ClusterRole")))), ) - if len(rules) == 0 { - return nil - } - return ptr.To(newClusterRole("", rules)) } // GenerateClusterExtensionFinalizerPolicyRule generates a policy rule that allows the holder to update @@ -73,26 +68,27 @@ func GenerateClusterExtensionFinalizerPolicyRule(clusterExtensionName string) rb } } -// GenerateResourceManagerRoles generates one or more Roles with permissions to manage objs resources in their +// GenerateResourceManagerRolePerms generates role permissions to manage objs resources in their // namespaces. The permissions also include any permissions defined in any Roles in objs within the namespace, allowing // the holder to also assign the RBAC therein to another service account. // Note: currently assumes objs have been created by convert.Convert. // The returned Roles will not have set .metadata.name -func GenerateResourceManagerRoles(objs []client.Object) []*rbacv1.Role { - return mapToSlice(slicesutil.GroupBy(slicesutil.Filter(objs, isNamespaceScopedResource), namespaceName), generateRole) -} - -func generateRole(namespace string, namespaceObjs []client.Object) *rbacv1.Role { - return ptr.To(newRole( - namespace, - "", - slices.Concat( - // namespace scoped resource creation and management rules - generatePolicyRules(namespaceObjs), - // controller rbac scope - collectRBACResourcePolicyRules(slicesutil.Filter(namespaceObjs, slicesutil.And(isOfKind("Role"), isGeneratedResource))), - ), - )) +func GenerateResourceManagerRolePerms(objs []client.Object) map[string][]rbacv1.PolicyRule { + out := map[string][]rbacv1.PolicyRule{} + namespaceScopedObjs := filter.Filter(objs, isNamespaceScopedResource) + for _, obj := range namespaceScopedObjs { + namespace := obj.GetNamespace() + if _, ok := out[namespace]; !ok { + objsInNamespace := filter.Filter(namespaceScopedObjs, isInNamespace(namespace)) + out[namespace] = slices.Concat( + // namespace scoped resource creation and management rules + generatePolicyRules(objsInNamespace), + // controller rbac scope + collectRBACResourcePolicyRules(filter.Filter(objsInNamespace, filter.And(isOfKind("Role"), isGeneratedResource))), + ) + } + } + return out } func generatePolicyRules(objs []client.Object) []rbacv1.PolicyRule { @@ -143,7 +139,7 @@ func isNamespaceScopedResource(o client.Object) bool { return slices.Contains(namespaceScopedResources, o.GetObjectKind().GroupVersionKind().Kind) } -func isOfKind(kind string) slicesutil.Predicate[client.Object] { +func isOfKind(kind string) filter.Predicate[client.Object] { return func(o client.Object) bool { return o.GetObjectKind().GroupVersionKind().Kind == kind } @@ -155,12 +151,14 @@ func isGeneratedResource(o client.Object) bool { return ok } -func groupKind(obj client.Object) schema.GroupKind { - return obj.GetObjectKind().GroupVersionKind().GroupKind() +func isInNamespace(namespace string) filter.Predicate[client.Object] { + return func(o client.Object) bool { + return o.GetNamespace() == namespace + } } -func namespaceName(obj client.Object) string { - return obj.GetNamespace() +func groupKind(obj client.Object) schema.GroupKind { + return obj.GetObjectKind().GroupVersionKind().GroupKind() } func toResourceName(o client.Object) string { diff --git a/internal/shared/util/filter/filter.go b/internal/shared/util/filter/filter.go index 885816283..96d60cfcd 100644 --- a/internal/shared/util/filter/filter.go +++ b/internal/shared/util/filter/filter.go @@ -5,10 +5,6 @@ import "slices" // Predicate returns true if the object should be kept when filtering type Predicate[T any] func(entity T) bool -type Key[T any, K comparable] func(entity T) K - -type MapFn[S any, V any] func(S) V - func And[T any](predicates ...Predicate[T]) Predicate[T] { return func(obj T) bool { for _, predicate := range predicates { @@ -54,20 +50,3 @@ func Filter[T any](s []T, test Predicate[T]) []T { func InPlace[T any](s []T, test Predicate[T]) []T { return slices.DeleteFunc(s, Not(test)) } - -func GroupBy[T any, K comparable](s []T, key Key[T, K]) map[K][]T { - out := map[K][]T{} - for _, value := range s { - k := key(value) - out[k] = append(out[k], value) - } - return out -} - -func Map[S, V any](s []S, mapper MapFn[S, V]) []V { - out := make([]V, len(s)) - for i := 0; i < len(s); i++ { - out[i] = mapper(s[i]) - } - return out -} diff --git a/internal/shared/util/slices/slices.go b/internal/shared/util/slices/slices.go new file mode 100644 index 000000000..90c33edae --- /dev/null +++ b/internal/shared/util/slices/slices.go @@ -0,0 +1,22 @@ +package slices + +type Key[T any, K comparable] func(entity T) K + +type MapFn[S any, V any] func(S) V + +func GroupBy[T any, K comparable](s []T, key Key[T, K]) map[K][]T { + out := map[K][]T{} + for _, value := range s { + k := key(value) + out[k] = append(out[k], value) + } + return out +} + +func Map[S, V any](s []S, mapper MapFn[S, V]) []V { + out := make([]V, len(s)) + for i := 0; i < len(s); i++ { + out[i] = mapper(s[i]) + } + return out +} diff --git a/internal/shared/util/slices/slices_test.go b/internal/shared/util/slices/slices_test.go new file mode 100644 index 000000000..5edf906ab --- /dev/null +++ b/internal/shared/util/slices/slices_test.go @@ -0,0 +1,31 @@ +package slices_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/slices" +) + +func Test_Map(t *testing.T) { + in := []int{1, 2, 3, 4, 5} + doubleIt := func(val int) int { + return 2 * val + } + require.Equal(t, []int{2, 4, 6, 8, 10}, slicesutil.Map(in, doubleIt)) +} + +func Test_GroupBy(t *testing.T) { + in := []int{1, 2, 3, 4, 5} + oddOrEven := func(val int) string { + if val%2 == 0 { + return "even" + } + return "odd" + } + require.Equal(t, map[string][]int{ + "even": {2, 4}, + "odd": {1, 3, 5}, + }, slicesutil.GroupBy(in, oddOrEven)) +} From 497f3ee221ff2be6121e55c456b9ea635d75bedc Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Tue, 4 Mar 2025 15:37:49 +0100 Subject: [PATCH 5/5] Add unit tests Signed-off-by: Per Goncalves da Silva --- .../rukpak/convert/installer_rbac_test.go | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 internal/operator-controller/rukpak/convert/installer_rbac_test.go diff --git a/internal/operator-controller/rukpak/convert/installer_rbac_test.go b/internal/operator-controller/rukpak/convert/installer_rbac_test.go new file mode 100644 index 000000000..deabdf0a4 --- /dev/null +++ b/internal/operator-controller/rukpak/convert/installer_rbac_test.go @@ -0,0 +1,283 @@ +package convert_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" +) + +func Test_GenerateResourceManagerClusterRolePerms_GeneratesRBACSuccessfully(t *testing.T) { + objs := []client.Object{ + // ClusterRole created by convert.Convert + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-controller-cluster-perms", + Annotations: map[string]string{ + convert.AnnotationRegistryV1GeneratedManifest: "", + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + // Some CRD + &apiextensions.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "CustomResourceDefinition", + APIVersion: apiextensions.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-controller-crd", + }, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "some.operator.domain", + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "operatorresources", + Kind: "OperatorResource", + ListKind: "OperatorResourceList", + Singular: "OperatorResource", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Served: true, + }, + }, + }, + }, + // Some Namespaced Resource + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-controller-config", + }, + Data: map[string]string{ + "some": "data", + }, + }, + } + + clusterRolePerms := convert.GenerateResourceManagerClusterRolePerms(objs) + require.ElementsMatch(t, []rbacv1.PolicyRule{ + // Aggregates operator-controller ClusterRole rules + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + Verbs: []string{"get", "list", "watch"}, + }, + // Adds cluster-scoped resource management rules + { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"clusterroles"}, + Verbs: []string{"create", "list", "watch"}, + }, { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"clusterroles"}, + Verbs: []string{"get", "update", "patch", "delete"}, + ResourceNames: []string{"operator-controller-cluster-perms"}, + }, { + APIGroups: []string{"apiextensions.k8s.io"}, + Resources: []string{"customresourcedefinitions"}, + Verbs: []string{"create", "list", "watch"}, + }, { + APIGroups: []string{"apiextensions.k8s.io"}, + Resources: []string{"customresourcedefinitions"}, + Verbs: []string{"get", "update", "patch", "delete"}, + ResourceNames: []string{"operator-controller-crd"}, + }, + // Nothing to be said about namespaced resources + }, clusterRolePerms) +} + +func Test_GenerateResourceManagerRolePerms_GeneratesRBACSuccessfully(t *testing.T) { + objs := []client.Object{ + // ClusterRole generated by convert.Convert - should be ignored + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-controller-namespace-perms", + Annotations: map[string]string{ + convert.AnnotationRegistryV1GeneratedManifest: "", + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + // Some cluster-scoped resources - should be ignored + &apiextensions.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "CustomResourceDefinition", + APIVersion: apiextensions.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-controller-crd", + }, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "some.operator.domain", + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "operatorresources", + Kind: "OperatorResource", + ListKind: "OperatorResourceList", + Singular: "OperatorResource", + }, + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Served: true, + }, + }, + }, + }, + // Some Namespaced Resource + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-controller-config", + Namespace: "install-namespace", + }, + Data: map[string]string{ + "some": "data", + }, + }, + // Some namespaces resource in a different namespace + &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-service", + Namespace: "another-namespace", + }, + }, + // Some convert.Convert generated Role - perms should be aggregated + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-controller-perms", + Namespace: "install-namespace", + Annotations: map[string]string{ + convert.AnnotationRegistryV1GeneratedManifest: "", + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + } + + namespaceRolePerms := convert.GenerateResourceManagerRolePerms(objs) + expected := map[string][]rbacv1.PolicyRule{ + "install-namespace": { + // Aggregates operator-controller ClusterRole rules + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"apps"}, + Resources: []string{"deployments"}, + Verbs: []string{"get", "list", "watch"}, + }, + // Adds cluster-scoped resource management rules + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"create", "list", "watch"}, + }, { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "update", "patch", "delete"}, + ResourceNames: []string{"operator-controller-config"}, + }, + { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"roles"}, + Verbs: []string{"create", "list", "watch"}, + }, { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"roles"}, + Verbs: []string{"get", "update", "patch", "delete"}, + ResourceNames: []string{"operator-controller-perms"}, + }, + }, + "another-namespace": { + { + APIGroups: []string{""}, + Resources: []string{"services"}, + Verbs: []string{"create", "list", "watch"}, + }, { + APIGroups: []string{""}, + Resources: []string{"services"}, + Verbs: []string{"get", "update", "patch", "delete"}, + ResourceNames: []string{"some-service"}, + }, + }, + } + + for namespace, perms := range namespaceRolePerms { + require.ElementsMatch(t, perms, expected[namespace]) + } +} + +func Test_GenerateClusterExtensionFinalizerPolicyRule(t *testing.T) { + rule := convert.GenerateClusterExtensionFinalizerPolicyRule("someext") + require.Equal(t, rbacv1.PolicyRule{ + APIGroups: []string{"olm.operatorframework.io"}, + Resources: []string{"clusterextensions/finalizers"}, + Verbs: []string{"update"}, + ResourceNames: []string{"someext"}, + }, rule) +}