|
1 | 1 | package convert |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "fmt" |
5 | | - "github.com/operator-framework/operator-controller/internal/shared/util/filter" |
6 | | - rbacv1 "k8s.io/api/rbac/v1" |
7 | | - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
8 | | - "k8s.io/apimachinery/pkg/runtime/schema" |
9 | | - "k8s.io/utils/ptr" |
10 | | - "sigs.k8s.io/controller-runtime/pkg/client" |
11 | | - "slices" |
12 | | - "strings" |
| 4 | + "fmt" |
| 5 | + "slices" |
| 6 | + "strings" |
| 7 | + |
| 8 | + rbacv1 "k8s.io/api/rbac/v1" |
| 9 | + "k8s.io/apimachinery/pkg/runtime/schema" |
| 10 | + "k8s.io/utils/ptr" |
| 11 | + "sigs.k8s.io/controller-runtime/pkg/client" |
| 12 | + |
| 13 | + slicesutil "github.com/operator-framework/operator-controller/internal/shared/util/filter" |
13 | 14 | ) |
14 | 15 |
|
15 | 16 | var ( |
16 | | - unnamedResourceVerbs = []string{"create", "list", "watch"} |
17 | | - namedResourceVerbs = []string{"get", "update", "patch", "delete"} |
18 | | - |
19 | | - // clusterScopedResources is a slice of registry+v1 bundle supported cluster scoped resource kinds |
20 | | - clusterScopedResources = []string{ |
21 | | - "ClusterRole", |
22 | | - "ClusterRoleBinding", |
23 | | - "PriorityClass", |
24 | | - "ConsoleYAMLSample", |
25 | | - "ConsoleQuickStart", |
26 | | - "ConsoleCLIDownload", |
27 | | - "ConsoleLink", |
28 | | - "CustomResourceDefinition", |
29 | | - } |
30 | | - |
31 | | - // clusterScopedResources is a slice of registry+v1 bundle supported namespace scoped resource kinds |
32 | | - namespaceScopedResources = []string{ |
33 | | - "Secret", |
34 | | - "ConfigMap", |
35 | | - "ServiceAccount", |
36 | | - "Service", |
37 | | - "Role", |
38 | | - "RoleBinding", |
39 | | - "PrometheusRule", |
40 | | - "ServiceMonitor", |
41 | | - "PodDisruptionBudget", |
42 | | - "VerticalPodAutoscaler", |
43 | | - "Deployment", |
44 | | - } |
| 17 | + unnamedResourceVerbs = []string{"create", "list", "watch"} |
| 18 | + namedResourceVerbs = []string{"get", "update", "patch", "delete"} |
| 19 | + |
| 20 | + // clusterScopedResources is a slice of registry+v1 bundle supported cluster scoped resource kinds |
| 21 | + clusterScopedResources = []string{ |
| 22 | + "ClusterRole", |
| 23 | + "ClusterRoleBinding", |
| 24 | + "PriorityClass", |
| 25 | + "ConsoleYAMLSample", |
| 26 | + "ConsoleQuickStart", |
| 27 | + "ConsoleCLIDownload", |
| 28 | + "ConsoleLink", |
| 29 | + "CustomResourceDefinition", |
| 30 | + } |
| 31 | + |
| 32 | + // clusterScopedResources is a slice of registry+v1 bundle supported namespace scoped resource kinds |
| 33 | + namespaceScopedResources = []string{ |
| 34 | + "Secret", |
| 35 | + "ConfigMap", |
| 36 | + "ServiceAccount", |
| 37 | + "Service", |
| 38 | + "Role", |
| 39 | + "RoleBinding", |
| 40 | + "PrometheusRule", |
| 41 | + "ServiceMonitor", |
| 42 | + "PodDisruptionBudget", |
| 43 | + "VerticalPodAutoscaler", |
| 44 | + "Deployment", |
| 45 | + } |
45 | 46 | ) |
46 | 47 |
|
47 | 48 | // GenerateResourceManagerClusterRole generates a ClusterRole with permissions to manage objs resources. The |
48 | 49 | // permissions also aggregate any permissions from any ClusterRoles in objs allowing the holder to also assign |
49 | | -// the RBAC therein to another service account. Note: currently assumes objs have been created by convert.Convert. |
| 50 | +// the RBAC therein to another service account. Note: assumes objs have been created by convert.Convert. |
| 51 | +// The returned ClusterRole will not have set .metadata.name |
50 | 52 | func GenerateResourceManagerClusterRole(objs []client.Object) *rbacv1.ClusterRole { |
51 | | - return ptr.To(newClusterRole( |
52 | | - "", |
53 | | - slices.Concat( |
54 | | - // cluster scoped resource creation and management rules |
55 | | - generatePolicyRules(filter.Filter(objs, isClusterScopedResource)), |
56 | | - // controller rbac scope |
57 | | - collectRBACResourcePolicyRules(filter.Filter(objs, filter.And(isGeneratedResource, isOfKind("ClusterRole")))), |
58 | | - ), |
59 | | - )) |
| 53 | + rules := slices.Concat( |
| 54 | + // cluster scoped resource creation and management rules |
| 55 | + generatePolicyRules(slicesutil.Filter(objs, isClusterScopedResource)), |
| 56 | + // controller rbac scope |
| 57 | + collectRBACResourcePolicyRules(slicesutil.Filter(objs, slicesutil.And(isGeneratedResource, isOfKind("ClusterRole")))), |
| 58 | + ) |
| 59 | + if len(rules) == 0 { |
| 60 | + return nil |
| 61 | + } |
| 62 | + return ptr.To(newClusterRole("", rules)) |
60 | 63 | } |
61 | 64 |
|
62 | 65 | // GenerateClusterExtensionFinalizerPolicyRule generates a policy rule that allows the holder to update |
63 | 66 | // finalizer for a ClusterExtension with clusterExtensionName. |
64 | 67 | func GenerateClusterExtensionFinalizerPolicyRule(clusterExtensionName string) rbacv1.PolicyRule { |
65 | | - return rbacv1.PolicyRule{ |
66 | | - APIGroups: []string{"olm.operatorframework.io"}, |
67 | | - Resources: []string{"clusterextensions/finalizers"}, |
68 | | - Verbs: []string{"update"}, |
69 | | - ResourceNames: []string{clusterExtensionName}, |
70 | | - } |
| 68 | + return rbacv1.PolicyRule{ |
| 69 | + APIGroups: []string{"olm.operatorframework.io"}, |
| 70 | + Resources: []string{"clusterextensions/finalizers"}, |
| 71 | + Verbs: []string{"update"}, |
| 72 | + ResourceNames: []string{clusterExtensionName}, |
| 73 | + } |
71 | 74 | } |
72 | 75 |
|
73 | 76 | // GenerateResourceManagerRoles generates one or more Roles with permissions to manage objs resources in their |
74 | 77 | // namespaces. The permissions also include any permissions defined in any Roles in objs within the namespace, allowing |
75 | 78 | // the holder to also assign the RBAC therein to another service account. |
76 | 79 | // Note: currently assumes objs have been created by convert.Convert. |
| 80 | +// The returned Roles will not have set .metadata.name |
77 | 81 | func GenerateResourceManagerRoles(objs []client.Object) []*rbacv1.Role { |
78 | | - return mapToSlice(filter.GroupBy(objs, namespaceName), generateRole) |
| 82 | + return mapToSlice(slicesutil.GroupBy(slicesutil.Filter(objs, isNamespaceScopedResource), namespaceName), generateRole) |
79 | 83 | } |
80 | 84 |
|
81 | 85 | func generateRole(namespace string, namespaceObjs []client.Object) *rbacv1.Role { |
82 | | - return ptr.To(newRole( |
83 | | - namespace, |
84 | | - "", |
85 | | - slices.Concat( |
86 | | - // namespace scoped resource creation and management rules |
87 | | - generatePolicyRules(namespaceObjs), |
88 | | - // controller rbac scope |
89 | | - collectRBACResourcePolicyRules(filter.Filter(namespaceObjs, filter.And(isOfKind("Role"), isGeneratedResource))), |
90 | | - ), |
91 | | - )) |
| 86 | + return ptr.To(newRole( |
| 87 | + namespace, |
| 88 | + "", |
| 89 | + slices.Concat( |
| 90 | + // namespace scoped resource creation and management rules |
| 91 | + generatePolicyRules(namespaceObjs), |
| 92 | + // controller rbac scope |
| 93 | + collectRBACResourcePolicyRules(slicesutil.Filter(namespaceObjs, slicesutil.And(isOfKind("Role"), isGeneratedResource))), |
| 94 | + ), |
| 95 | + )) |
92 | 96 | } |
93 | 97 |
|
94 | 98 | func generatePolicyRules(objs []client.Object) []rbacv1.PolicyRule { |
95 | | - return slices.Concat(mapToSlice(filter.GroupBy(objs, groupKind), func(gk schema.GroupKind, resources []client.Object) []rbacv1.PolicyRule { |
96 | | - return []rbacv1.PolicyRule{ |
97 | | - newPolicyRule(gk, unnamedResourceVerbs), |
98 | | - newPolicyRule(gk, namedResourceVerbs, filter.Map(resources, toResourceName)...), |
99 | | - } |
100 | | - })...) |
| 99 | + return slices.Concat( |
| 100 | + mapToSlice(slicesutil.GroupBy(objs, groupKind), func(gk schema.GroupKind, resources []client.Object) []rbacv1.PolicyRule { |
| 101 | + return []rbacv1.PolicyRule{ |
| 102 | + newPolicyRule(gk, unnamedResourceVerbs), |
| 103 | + newPolicyRule(gk, namedResourceVerbs, slicesutil.Map(resources, toResourceName)...), |
| 104 | + } |
| 105 | + })..., |
| 106 | + ) |
101 | 107 | } |
102 | 108 |
|
103 | 109 | func collectRBACResourcePolicyRules(objs []client.Object) []rbacv1.PolicyRule { |
104 | | - return slices.Concat(filter.Map(objs, func(obj client.Object) []rbacv1.PolicyRule { |
105 | | - if cr, ok := obj.(*rbacv1.ClusterRole); ok { |
106 | | - return cr.Rules |
107 | | - } else if r, ok := obj.(*rbacv1.Role); ok { |
108 | | - return r.Rules |
109 | | - } else { |
110 | | - panic(fmt.Sprintf("unexpected type %T", obj)) |
111 | | - } |
112 | | - })...) |
| 110 | + return slices.Concat(slicesutil.Map(objs, func(obj client.Object) []rbacv1.PolicyRule { |
| 111 | + if cr, ok := obj.(*rbacv1.ClusterRole); ok { |
| 112 | + return cr.Rules |
| 113 | + } else if r, ok := obj.(*rbacv1.Role); ok { |
| 114 | + return r.Rules |
| 115 | + } else { |
| 116 | + panic(fmt.Sprintf("unexpected type %T", obj)) |
| 117 | + } |
| 118 | + })...) |
113 | 119 | } |
114 | 120 |
|
115 | 121 | func newPolicyRule(groupKind schema.GroupKind, verbs []string, resourceNames ...string) rbacv1.PolicyRule { |
116 | | - return rbacv1.PolicyRule{ |
117 | | - APIGroups: []string{groupKind.Group}, |
118 | | - Resources: []string{fmt.Sprintf("%ss", strings.ToLower(groupKind.Kind))}, |
119 | | - Verbs: verbs, |
120 | | - ResourceNames: resourceNames, |
121 | | - } |
| 122 | + return rbacv1.PolicyRule{ |
| 123 | + APIGroups: []string{groupKind.Group}, |
| 124 | + Resources: []string{fmt.Sprintf("%ss", strings.ToLower(groupKind.Kind))}, |
| 125 | + Verbs: verbs, |
| 126 | + ResourceNames: resourceNames, |
| 127 | + } |
122 | 128 | } |
123 | 129 |
|
124 | 130 | func mapToSlice[K comparable, V any, R any](m map[K]V, fn func(k K, v V) R) []R { |
125 | | - out := make([]R, 0, len(m)) |
126 | | - for k, v := range m { |
127 | | - out = append(out, fn(k, v)) |
128 | | - } |
129 | | - return out |
| 131 | + out := make([]R, 0, len(m)) |
| 132 | + for k, v := range m { |
| 133 | + out = append(out, fn(k, v)) |
| 134 | + } |
| 135 | + return out |
130 | 136 | } |
131 | 137 |
|
132 | 138 | func isClusterScopedResource(o client.Object) bool { |
133 | | - return slices.Contains(clusterScopedResources, o.GetObjectKind().GroupVersionKind().Kind) |
| 139 | + return slices.Contains(clusterScopedResources, o.GetObjectKind().GroupVersionKind().Kind) |
| 140 | +} |
| 141 | + |
| 142 | +func isNamespaceScopedResource(o client.Object) bool { |
| 143 | + return slices.Contains(namespaceScopedResources, o.GetObjectKind().GroupVersionKind().Kind) |
134 | 144 | } |
135 | 145 |
|
136 | | -func isOfKind(kind string) filter.Predicate[client.Object] { |
137 | | - return func(o client.Object) bool { |
138 | | - return o.GetObjectKind().GroupVersionKind().Kind == kind |
139 | | - } |
| 146 | +func isOfKind(kind string) slicesutil.Predicate[client.Object] { |
| 147 | + return func(o client.Object) bool { |
| 148 | + return o.GetObjectKind().GroupVersionKind().Kind == kind |
| 149 | + } |
140 | 150 | } |
141 | 151 |
|
142 | 152 | func isGeneratedResource(o client.Object) bool { |
143 | | - // TODO: this is a hack that abuses an internal implementation detail |
144 | | - // we should probably annotate the generated resources coming out of convert.Convert |
145 | | - _, ok := o.(*unstructured.Unstructured) |
146 | | - return ok |
| 153 | + annotations := o.GetAnnotations() |
| 154 | + _, ok := annotations[AnnotationRegistryV1GeneratedManifest] |
| 155 | + return ok |
147 | 156 | } |
148 | 157 |
|
149 | 158 | func groupKind(obj client.Object) schema.GroupKind { |
150 | | - return obj.GetObjectKind().GroupVersionKind().GroupKind() |
| 159 | + return obj.GetObjectKind().GroupVersionKind().GroupKind() |
151 | 160 | } |
152 | 161 |
|
153 | 162 | func namespaceName(obj client.Object) string { |
154 | | - return obj.GetNamespace() |
| 163 | + return obj.GetNamespace() |
155 | 164 | } |
156 | 165 |
|
157 | 166 | func toResourceName(o client.Object) string { |
158 | | - return o.GetName() |
| 167 | + return o.GetName() |
159 | 168 | } |
0 commit comments