Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 33dbb54

Browse files
author
Per Goncalves da Silva
committedApr 11, 2025·
Replace rukpak Convert() with BundleRenderer
Signed-off-by: Per Goncalves da Silva <[email protected]>
1 parent ed205c5 commit 33dbb54

File tree

14 files changed

+2386
-412
lines changed

14 files changed

+2386
-412
lines changed
 

‎internal/operator-controller/rukpak/convert/registryv1.go

Lines changed: 21 additions & 269 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,25 @@ import (
77
"fmt"
88
"io/fs"
99
"path/filepath"
10-
"strings"
1110

1211
"helm.sh/helm/v3/pkg/chart"
13-
appsv1 "k8s.io/api/apps/v1"
14-
corev1 "k8s.io/api/core/v1"
15-
rbacv1 "k8s.io/api/rbac/v1"
1612
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
17-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1813
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1914
"k8s.io/apimachinery/pkg/runtime"
2015
"k8s.io/apimachinery/pkg/util/sets"
2116
"k8s.io/cli-runtime/pkg/resource"
22-
"k8s.io/utils/ptr"
2317
"sigs.k8s.io/controller-runtime/pkg/client"
2418
"sigs.k8s.io/yaml"
2519

2620
"github.com/operator-framework/api/pkg/operators/v1alpha1"
2721
"github.com/operator-framework/operator-registry/alpha/property"
28-
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
2922

3023
registry "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/operator-registry"
31-
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
24+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
25+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/generators"
26+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/validators"
3227
)
3328

34-
type RegistryV1 struct {
35-
PackageName string
36-
CSV v1alpha1.ClusterServiceVersion
37-
CRDs []apiextensionsv1.CustomResourceDefinition
38-
Others []unstructured.Unstructured
39-
}
40-
4129
type Plain struct {
4230
Objects []client.Object
4331
}
@@ -70,16 +58,16 @@ func RegistryV1ToHelmChart(rv1 fs.FS, installNamespace string, watchNamespace st
7058
return chrt, nil
7159
}
7260

73-
// ParseFS converts the rv1 filesystem into a RegistryV1.
61+
// ParseFS converts the rv1 filesystem into a render.RegistryV1.
7462
// ParseFS expects the filesystem to conform to the registry+v1 format:
7563
// metadata/annotations.yaml
7664
// manifests/
7765
// - csv.yaml
7866
// - ...
7967
//
8068
// manifests directory does not contain subdirectories
81-
func ParseFS(rv1 fs.FS) (RegistryV1, error) {
82-
reg := RegistryV1{}
69+
func ParseFS(rv1 fs.FS) (render.RegistryV1, error) {
70+
reg := render.RegistryV1{}
8371
annotationsFileData, err := fs.ReadFile(rv1, filepath.Join("metadata", "annotations.yaml"))
8472
if err != nil {
8573
return reg, err
@@ -224,22 +212,23 @@ func validateTargetNamespaces(supportedInstallModes sets.Set[string], installNam
224212
return fmt.Errorf("supported install modes %v do not support target namespaces %v", sets.List[string](supportedInstallModes), targetNamespaces)
225213
}
226214

227-
func saNameOrDefault(saName string) string {
228-
if saName == "" {
229-
return "default"
230-
}
231-
return saName
215+
var PlainConverter = Converter{
216+
BundleRenderer: render.BundleRenderer{
217+
BundleValidator: validators.RegistryV1BundleValidator,
218+
ResourceGenerators: []render.ResourceGenerator{
219+
generators.BundleCSVRBACResourceGenerator.ResourceGenerator(),
220+
generators.BundleCRDGenerator,
221+
generators.BundleAdditionalResourcesGenerator,
222+
generators.BundleCSVDeploymentGenerator,
223+
},
224+
},
232225
}
233226

234227
type Converter struct {
235-
BundleValidator BundleValidator
228+
render.BundleRenderer
236229
}
237230

238-
func (c Converter) Convert(rv1 RegistryV1, installNamespace string, targetNamespaces []string) (*Plain, error) {
239-
if err := c.BundleValidator.Validate(&rv1); err != nil {
240-
return nil, err
241-
}
242-
231+
func (c Converter) Convert(rv1 render.RegistryV1, installNamespace string, targetNamespaces []string) (*Plain, error) {
243232
if installNamespace == "" {
244233
installNamespace = rv1.CSV.Annotations["operatorframework.io/suggested-namespace"]
245234
}
@@ -272,246 +261,9 @@ func (c Converter) Convert(rv1 RegistryV1, installNamespace string, targetNamesp
272261
return nil, fmt.Errorf("webhookDefinitions are not supported")
273262
}
274263

275-
deployments := []appsv1.Deployment{}
276-
serviceAccounts := map[string]corev1.ServiceAccount{}
277-
for _, depSpec := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs {
278-
annotations := util.MergeMaps(rv1.CSV.Annotations, depSpec.Spec.Template.Annotations)
279-
annotations["olm.targetNamespaces"] = strings.Join(targetNamespaces, ",")
280-
depSpec.Spec.Template.Annotations = annotations
281-
282-
// Hardcode the deployment with RevisionHistoryLimit=1 to replicate OLMv0 behavior
283-
// https://github.com/operator-framework/operator-lifecycle-manager/blob/dfd0b2bea85038d3c0d65348bc812d297f16b8d2/pkg/controller/install/deployment.go#L181
284-
depSpec.Spec.RevisionHistoryLimit = ptr.To(int32(1))
285-
286-
deployments = append(deployments, appsv1.Deployment{
287-
TypeMeta: metav1.TypeMeta{
288-
Kind: "Deployment",
289-
APIVersion: appsv1.SchemeGroupVersion.String(),
290-
},
291-
292-
ObjectMeta: metav1.ObjectMeta{
293-
Namespace: installNamespace,
294-
Name: depSpec.Name,
295-
Labels: depSpec.Label,
296-
},
297-
Spec: depSpec.Spec,
298-
})
299-
saName := saNameOrDefault(depSpec.Spec.Template.Spec.ServiceAccountName)
300-
serviceAccounts[saName] = newServiceAccount(installNamespace, saName)
301-
}
302-
303-
// NOTES:
304-
// 1. There's an extra Role for OperatorConditions: get/update/patch; resourceName=csv.name
305-
// - This is managed by the OperatorConditions controller here: https://github.com/operator-framework/operator-lifecycle-manager/blob/9ced412f3e263b8827680dc0ad3477327cd9a508/pkg/controller/operators/operatorcondition_controller.go#L106-L109
306-
// 2. There's an extra RoleBinding for the above mentioned role.
307-
// - Every SA mentioned in the OperatorCondition.spec.serviceAccounts is a subject for this role binding: https://github.com/operator-framework/operator-lifecycle-manager/blob/9ced412f3e263b8827680dc0ad3477327cd9a508/pkg/controller/operators/operatorcondition_controller.go#L171-L177
308-
// 3. strategySpec.permissions are _also_ given a clusterrole/clusterrole binding.
309-
// - (for AllNamespaces mode only?)
310-
// - (where does the extra namespaces get/list/watch rule come from?)
311-
312-
roles := []rbacv1.Role{}
313-
roleBindings := []rbacv1.RoleBinding{}
314-
clusterRoles := []rbacv1.ClusterRole{}
315-
clusterRoleBindings := []rbacv1.ClusterRoleBinding{}
316-
317-
permissions := rv1.CSV.Spec.InstallStrategy.StrategySpec.Permissions
318-
clusterPermissions := rv1.CSV.Spec.InstallStrategy.StrategySpec.ClusterPermissions
319-
allPermissions := append(permissions, clusterPermissions...)
320-
321-
// Create all the service accounts
322-
for _, permission := range allPermissions {
323-
saName := saNameOrDefault(permission.ServiceAccountName)
324-
if _, ok := serviceAccounts[saName]; !ok {
325-
serviceAccounts[saName] = newServiceAccount(installNamespace, saName)
326-
}
327-
}
328-
329-
// If we're in AllNamespaces mode, promote the permissions to clusterPermissions
330-
if len(targetNamespaces) == 1 && targetNamespaces[0] == "" {
331-
for _, p := range permissions {
332-
p.Rules = append(p.Rules, rbacv1.PolicyRule{
333-
Verbs: []string{"get", "list", "watch"},
334-
APIGroups: []string{corev1.GroupName},
335-
Resources: []string{"namespaces"},
336-
})
337-
clusterPermissions = append(clusterPermissions, p)
338-
}
339-
permissions = nil
340-
}
341-
342-
for _, ns := range targetNamespaces {
343-
for _, permission := range permissions {
344-
saName := saNameOrDefault(permission.ServiceAccountName)
345-
name, err := generateName(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission)
346-
if err != nil {
347-
return nil, err
348-
}
349-
roles = append(roles, newRole(ns, name, permission.Rules))
350-
roleBindings = append(roleBindings, newRoleBinding(ns, name, name, installNamespace, saName))
351-
}
352-
}
353-
354-
for _, permission := range clusterPermissions {
355-
saName := saNameOrDefault(permission.ServiceAccountName)
356-
name, err := generateName(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission)
357-
if err != nil {
358-
return nil, err
359-
}
360-
clusterRoles = append(clusterRoles, newClusterRole(name, permission.Rules))
361-
clusterRoleBindings = append(clusterRoleBindings, newClusterRoleBinding(name, name, installNamespace, saName))
362-
}
363-
364-
objs := []client.Object{}
365-
for _, obj := range serviceAccounts {
366-
obj := obj
367-
if obj.GetName() != "default" {
368-
objs = append(objs, &obj)
369-
}
370-
}
371-
for _, obj := range roles {
372-
obj := obj
373-
objs = append(objs, &obj)
374-
}
375-
for _, obj := range roleBindings {
376-
obj := obj
377-
objs = append(objs, &obj)
378-
}
379-
for _, obj := range clusterRoles {
380-
obj := obj
381-
objs = append(objs, &obj)
382-
}
383-
for _, obj := range clusterRoleBindings {
384-
obj := obj
385-
objs = append(objs, &obj)
386-
}
387-
for _, obj := range rv1.CRDs {
388-
objs = append(objs, &obj)
389-
}
390-
for _, obj := range rv1.Others {
391-
obj := obj
392-
supported, namespaced := registrybundle.IsSupported(obj.GetKind())
393-
if !supported {
394-
return nil, fmt.Errorf("bundle contains unsupported resource: Name: %v, Kind: %v", obj.GetName(), obj.GetKind())
395-
}
396-
if namespaced {
397-
obj.SetNamespace(installNamespace)
398-
}
399-
objs = append(objs, &obj)
400-
}
401-
for _, obj := range deployments {
402-
obj := obj
403-
objs = append(objs, &obj)
404-
}
405-
return &Plain{Objects: objs}, nil
406-
}
407-
408-
var PlainConverter = Converter{
409-
BundleValidator: RegistryV1BundleValidator,
410-
}
411-
412-
const maxNameLength = 63
413-
414-
func generateName(base string, o interface{}) (string, error) {
415-
hashStr, err := util.DeepHashObject(o)
264+
objs, err := c.BundleRenderer.Render(rv1, installNamespace, targetNamespaces)
416265
if err != nil {
417-
return "", err
418-
}
419-
if len(base)+len(hashStr) > maxNameLength {
420-
base = base[:maxNameLength-len(hashStr)-1]
421-
}
422-
423-
return fmt.Sprintf("%s-%s", base, hashStr), nil
424-
}
425-
426-
func newServiceAccount(namespace, name string) corev1.ServiceAccount {
427-
return corev1.ServiceAccount{
428-
TypeMeta: metav1.TypeMeta{
429-
Kind: "ServiceAccount",
430-
APIVersion: corev1.SchemeGroupVersion.String(),
431-
},
432-
ObjectMeta: metav1.ObjectMeta{
433-
Namespace: namespace,
434-
Name: name,
435-
},
436-
}
437-
}
438-
439-
func newRole(namespace, name string, rules []rbacv1.PolicyRule) rbacv1.Role {
440-
return rbacv1.Role{
441-
TypeMeta: metav1.TypeMeta{
442-
Kind: "Role",
443-
APIVersion: rbacv1.SchemeGroupVersion.String(),
444-
},
445-
ObjectMeta: metav1.ObjectMeta{
446-
Namespace: namespace,
447-
Name: name,
448-
},
449-
Rules: rules,
450-
}
451-
}
452-
453-
func newClusterRole(name string, rules []rbacv1.PolicyRule) rbacv1.ClusterRole {
454-
return rbacv1.ClusterRole{
455-
TypeMeta: metav1.TypeMeta{
456-
Kind: "ClusterRole",
457-
APIVersion: rbacv1.SchemeGroupVersion.String(),
458-
},
459-
ObjectMeta: metav1.ObjectMeta{
460-
Name: name,
461-
},
462-
Rules: rules,
463-
}
464-
}
465-
466-
func newRoleBinding(namespace, name, roleName, saNamespace string, saNames ...string) rbacv1.RoleBinding {
467-
subjects := make([]rbacv1.Subject, 0, len(saNames))
468-
for _, saName := range saNames {
469-
subjects = append(subjects, rbacv1.Subject{
470-
Kind: "ServiceAccount",
471-
Namespace: saNamespace,
472-
Name: saName,
473-
})
474-
}
475-
return rbacv1.RoleBinding{
476-
TypeMeta: metav1.TypeMeta{
477-
Kind: "RoleBinding",
478-
APIVersion: rbacv1.SchemeGroupVersion.String(),
479-
},
480-
ObjectMeta: metav1.ObjectMeta{
481-
Namespace: namespace,
482-
Name: name,
483-
},
484-
Subjects: subjects,
485-
RoleRef: rbacv1.RoleRef{
486-
APIGroup: rbacv1.GroupName,
487-
Kind: "Role",
488-
Name: roleName,
489-
},
490-
}
491-
}
492-
493-
func newClusterRoleBinding(name, roleName, saNamespace string, saNames ...string) rbacv1.ClusterRoleBinding {
494-
subjects := make([]rbacv1.Subject, 0, len(saNames))
495-
for _, saName := range saNames {
496-
subjects = append(subjects, rbacv1.Subject{
497-
Kind: "ServiceAccount",
498-
Namespace: saNamespace,
499-
Name: saName,
500-
})
501-
}
502-
return rbacv1.ClusterRoleBinding{
503-
TypeMeta: metav1.TypeMeta{
504-
Kind: "ClusterRoleBinding",
505-
APIVersion: rbacv1.SchemeGroupVersion.String(),
506-
},
507-
ObjectMeta: metav1.ObjectMeta{
508-
Name: name,
509-
},
510-
Subjects: subjects,
511-
RoleRef: rbacv1.RoleRef{
512-
APIGroup: rbacv1.GroupName,
513-
Kind: "ClusterRole",
514-
Name: roleName,
515-
},
266+
return nil, err
516267
}
268+
return &Plain{Objects: objs}, nil
517269
}

‎internal/operator-controller/rukpak/convert/registryv1_test.go

Lines changed: 56 additions & 50 deletions
Large diffs are not rendered by default.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package generators
2+
3+
import (
4+
"cmp"
5+
"fmt"
6+
"strings"
7+
8+
corev1 "k8s.io/api/core/v1"
9+
rbacv1 "k8s.io/api/rbac/v1"
10+
"k8s.io/apimachinery/pkg/util/sets"
11+
"k8s.io/utils/ptr"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
14+
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
15+
16+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
17+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
18+
)
19+
20+
// BundleCSVRBACResourceGenerator generates all ServiceAccounts, ClusterRoles, ClusterRoleBindings, Roles, RoleBindings
21+
// defined in the RegistryV1 bundle's cluster service version (CSV)
22+
var BundleCSVRBACResourceGenerator = render.ResourceGenerators{
23+
BundleCSVServiceAccountGenerator,
24+
BundleCSVPermissionsGenerator,
25+
BundleCSVClusterPermissionsGenerator,
26+
}
27+
28+
// BundleCSVDeploymentGenerator generates all deployments defined in rv1's cluster service version (CSV). The generated
29+
// resource aim to have parity with OLMv0 generated Deployment resources:
30+
// - olm.targetNamespaces annotation is set with the opts.TargetNamespace value
31+
// - the deployment spec's revision history limit is set to 1
32+
// - merges csv annotations to the deployment template's annotations
33+
func BundleCSVDeploymentGenerator(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
34+
if rv1 == nil {
35+
return nil, fmt.Errorf("bundle cannot be nil")
36+
}
37+
objs := make([]client.Object, 0, len(rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs))
38+
for _, depSpec := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs {
39+
// Add CSV annotations to template annotations
40+
// See https://github.com/operator-framework/operator-lifecycle-manager/blob/dfd0b2bea85038d3c0d65348bc812d297f16b8d2/pkg/controller/install/deployment.go#L142
41+
annotations := util.MergeMaps(rv1.CSV.Annotations, depSpec.Spec.Template.Annotations)
42+
43+
// In OLMv0 CSVs are annotated with the OperatorGroup's .spec.targetNamespaces
44+
// See https://github.com/operator-framework/operator-lifecycle-manager/blob/dfd0b2bea85038d3c0d65348bc812d297f16b8d2/pkg/controller/operators/olm/operatorgroup.go#L279
45+
// When the CSVs annotations are copied to the deployment template's annotations, they bring with it this annotation
46+
annotations["olm.targetNamespaces"] = strings.Join(opts.TargetNamespaces, ",")
47+
depSpec.Spec.Template.Annotations = annotations
48+
49+
// Hardcode the deployment with RevisionHistoryLimit=1 to maintain parity with OLMv0 behaviour.
50+
// See https://github.com/operator-framework/operator-lifecycle-manager/blob/dfd0b2bea85038d3c0d65348bc812d297f16b8d2/pkg/controller/install/deployment.go#L177-L180
51+
depSpec.Spec.RevisionHistoryLimit = ptr.To(int32(1))
52+
53+
objs = append(objs,
54+
CreateDeploymentResource(
55+
depSpec.Name,
56+
opts.InstallNamespace,
57+
WithDeploymentSpec(depSpec.Spec),
58+
WithLabels(depSpec.Label),
59+
),
60+
)
61+
}
62+
return objs, nil
63+
}
64+
65+
// BundleCSVPermissionsGenerator generates the Roles and RoleBindings based on bundle's cluster service version
66+
// permission spec. If the bundle is being installed in AllNamespaces mode (opts.TargetNamespaces = [”])
67+
// no resources will be generated as these permissions will be promoted to ClusterRole/Bunding(s)
68+
func BundleCSVPermissionsGenerator(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
69+
if rv1 == nil {
70+
return nil, fmt.Errorf("bundle cannot be nil")
71+
}
72+
73+
// If we're in AllNamespaces mode permissions will be treated as clusterPermissions
74+
if len(opts.TargetNamespaces) == 1 && opts.TargetNamespaces[0] == "" {
75+
return nil, nil
76+
}
77+
78+
permissions := rv1.CSV.Spec.InstallStrategy.StrategySpec.Permissions
79+
80+
objs := make([]client.Object, 0, 2*len(opts.TargetNamespaces)*len(permissions))
81+
for _, ns := range opts.TargetNamespaces {
82+
for _, permission := range permissions {
83+
saName := saNameOrDefault(permission.ServiceAccountName)
84+
name, err := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission)
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
objs = append(objs,
90+
CreateRoleResource(name, ns, WithRules(permission.Rules...)),
91+
CreateRoleBindingResource(
92+
name,
93+
ns,
94+
WithSubjects(rbacv1.Subject{Kind: "ServiceAccount", Namespace: opts.InstallNamespace, Name: saName}),
95+
WithRoleRef(rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: name}),
96+
),
97+
)
98+
}
99+
}
100+
return objs, nil
101+
}
102+
103+
// BundleCSVClusterPermissionsGenerator generates ClusterRoles and ClusterRoleBindings based on the bundle's
104+
// cluster service version clusterPermission spec. If the bundle is being installed in AllNamespaces mode
105+
// (opts.TargetNamespaces = [”]), the CSV's permission spec will be promoted to ClusterRole and ClusterRoleBinding
106+
// resources. To keep parity with OLMv0, these will also include an extra rule to get, list, watch namespaces
107+
// (see https://github.com/operator-framework/operator-lifecycle-manager/blob/dfd0b2bea85038d3c0d65348bc812d297f16b8d2/pkg/controller/operators/olm/operatorgroup.go#L539)
108+
func BundleCSVClusterPermissionsGenerator(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
109+
if rv1 == nil {
110+
return nil, fmt.Errorf("bundle cannot be nil")
111+
}
112+
clusterPermissions := rv1.CSV.Spec.InstallStrategy.StrategySpec.ClusterPermissions
113+
114+
// If we're in AllNamespaces mode, promote the permissions to clusterPermissions
115+
if len(opts.TargetNamespaces) == 1 && opts.TargetNamespaces[0] == "" {
116+
for _, p := range rv1.CSV.Spec.InstallStrategy.StrategySpec.Permissions {
117+
p.Rules = append(p.Rules, rbacv1.PolicyRule{
118+
Verbs: []string{"get", "list", "watch"},
119+
APIGroups: []string{corev1.GroupName},
120+
Resources: []string{"namespaces"},
121+
})
122+
clusterPermissions = append(clusterPermissions, p)
123+
}
124+
}
125+
126+
objs := make([]client.Object, 0, 2*len(clusterPermissions))
127+
for _, permission := range clusterPermissions {
128+
saName := saNameOrDefault(permission.ServiceAccountName)
129+
name, err := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission)
130+
if err != nil {
131+
return nil, err
132+
}
133+
objs = append(objs,
134+
CreateClusterRoleResource(name, WithRules(permission.Rules...)),
135+
CreateClusterRoleBindingResource(
136+
name,
137+
WithSubjects(rbacv1.Subject{Kind: "ServiceAccount", Namespace: opts.InstallNamespace, Name: saName}),
138+
WithRoleRef(rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: name}),
139+
),
140+
)
141+
}
142+
return objs, nil
143+
}
144+
145+
// BundleCSVServiceAccountGenerator generates ServiceAccount resources based on the bundle's cluster service version
146+
// permission and clusterPermission spec. One ServiceAccount resource is created / referenced service account (i.e.
147+
// if multiple permissions reference the same service account, only one resource will be generated).
148+
// If a clusterPermission, or permission, references an empty (”) service account, this is considered to be the
149+
// namespace 'default' service account. A resource for the namespace 'default' service account is not generated.
150+
func BundleCSVServiceAccountGenerator(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
151+
if rv1 == nil {
152+
return nil, fmt.Errorf("bundle cannot be nil")
153+
}
154+
allPermissions := append(
155+
rv1.CSV.Spec.InstallStrategy.StrategySpec.Permissions,
156+
rv1.CSV.Spec.InstallStrategy.StrategySpec.ClusterPermissions...,
157+
)
158+
159+
serviceAccountNames := sets.Set[string]{}
160+
for _, permission := range allPermissions {
161+
serviceAccountNames.Insert(saNameOrDefault(permission.ServiceAccountName))
162+
}
163+
164+
objs := make([]client.Object, 0, len(serviceAccountNames))
165+
for _, serviceAccountName := range serviceAccountNames.UnsortedList() {
166+
// no need to generate the default service account
167+
if serviceAccountName != "default" {
168+
objs = append(objs, CreateServiceAccountResource(serviceAccountName, opts.InstallNamespace))
169+
}
170+
}
171+
return objs, nil
172+
}
173+
174+
// BundleCRDGenerator generates CustomResourceDefinition resources from the registry+v1 bundle
175+
func BundleCRDGenerator(rv1 *render.RegistryV1, _ render.Options) ([]client.Object, error) {
176+
if rv1 == nil {
177+
return nil, fmt.Errorf("bundle cannot be nil")
178+
}
179+
objs := make([]client.Object, 0, len(rv1.CRDs))
180+
for _, crd := range rv1.CRDs {
181+
objs = append(objs, crd.DeepCopy())
182+
}
183+
return objs, nil
184+
}
185+
186+
// BundleAdditionalResourcesGenerator generates resources for the additional resources included in the
187+
// bundle. If the bundle resource is namespace scoped, its namespace will be set to the value of opts.InstallNamespace.
188+
func BundleAdditionalResourcesGenerator(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
189+
if rv1 == nil {
190+
return nil, fmt.Errorf("bundle cannot be nil")
191+
}
192+
objs := make([]client.Object, 0, len(rv1.Others))
193+
for _, res := range rv1.Others {
194+
supported, namespaced := registrybundle.IsSupported(res.GetKind())
195+
if !supported {
196+
return nil, fmt.Errorf("bundle contains unsupported resource: Name: %v, Kind: %v", res.GetName(), res.GetKind())
197+
}
198+
199+
obj := res.DeepCopy()
200+
if namespaced {
201+
obj.SetNamespace(opts.InstallNamespace)
202+
}
203+
204+
objs = append(objs, obj)
205+
}
206+
return objs, nil
207+
}
208+
209+
func saNameOrDefault(saName string) string {
210+
return cmp.Or(saName, "default")
211+
}

‎internal/operator-controller/rukpak/render/generators/generators_test.go

Lines changed: 1183 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package generators
2+
3+
import (
4+
appsv1 "k8s.io/api/apps/v1"
5+
corev1 "k8s.io/api/core/v1"
6+
rbacv1 "k8s.io/api/rbac/v1"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"sigs.k8s.io/controller-runtime/pkg/client"
9+
)
10+
11+
type ResourceCreatorOption = func(client.Object)
12+
type ResourceCreatorOptions []ResourceCreatorOption
13+
14+
func (r ResourceCreatorOptions) ApplyTo(obj client.Object) client.Object {
15+
if obj == nil {
16+
return nil
17+
}
18+
for _, opt := range r {
19+
if opt != nil {
20+
opt(obj)
21+
}
22+
}
23+
return obj
24+
}
25+
26+
// WithSubjects applies rbac subjects to ClusterRoleBinding and RoleBinding resources
27+
func WithSubjects(subjects ...rbacv1.Subject) func(client.Object) {
28+
return func(obj client.Object) {
29+
switch o := obj.(type) {
30+
case *rbacv1.RoleBinding:
31+
o.Subjects = subjects
32+
case *rbacv1.ClusterRoleBinding:
33+
o.Subjects = subjects
34+
}
35+
}
36+
}
37+
38+
// WithRoleRef applies an rbac RoleRef to ClusterRoleBinding and RoleBinding resources
39+
func WithRoleRef(roleRef rbacv1.RoleRef) func(client.Object) {
40+
return func(obj client.Object) {
41+
switch o := obj.(type) {
42+
case *rbacv1.RoleBinding:
43+
o.RoleRef = roleRef
44+
case *rbacv1.ClusterRoleBinding:
45+
o.RoleRef = roleRef
46+
}
47+
}
48+
}
49+
50+
// WithRules applies rbac PolicyRules to Role and ClusterRole resources
51+
func WithRules(rules ...rbacv1.PolicyRule) func(client.Object) {
52+
return func(obj client.Object) {
53+
switch o := obj.(type) {
54+
case *rbacv1.Role:
55+
o.Rules = rules
56+
case *rbacv1.ClusterRole:
57+
o.Rules = rules
58+
}
59+
}
60+
}
61+
62+
// WithDeploymentSpec applies a DeploymentSpec to Deployment resources
63+
func WithDeploymentSpec(depSpec appsv1.DeploymentSpec) func(client.Object) {
64+
return func(obj client.Object) {
65+
switch o := obj.(type) {
66+
case *appsv1.Deployment:
67+
o.Spec = depSpec
68+
}
69+
}
70+
}
71+
72+
// WithLabels applies labels to the metadata of any resource
73+
func WithLabels(labels map[string]string) func(client.Object) {
74+
return func(obj client.Object) {
75+
obj.SetLabels(labels)
76+
}
77+
}
78+
79+
// CreateServiceAccountResource creates a ServiceAccount resource with name 'name', namespace 'namespace', and applying
80+
// any ServiceAccount related options in opts
81+
func CreateServiceAccountResource(name string, namespace string, opts ...ResourceCreatorOption) *corev1.ServiceAccount {
82+
return ResourceCreatorOptions(opts).ApplyTo(
83+
&corev1.ServiceAccount{
84+
TypeMeta: metav1.TypeMeta{
85+
Kind: "ServiceAccount",
86+
APIVersion: corev1.SchemeGroupVersion.String(),
87+
},
88+
ObjectMeta: metav1.ObjectMeta{
89+
Namespace: namespace,
90+
Name: name,
91+
},
92+
},
93+
).(*corev1.ServiceAccount)
94+
}
95+
96+
// CreateRoleResource creates a Role resource with name 'name' and namespace 'namespace' and applying any
97+
// Role related options in opts
98+
func CreateRoleResource(name string, namespace string, opts ...ResourceCreatorOption) *rbacv1.Role {
99+
return ResourceCreatorOptions(opts).ApplyTo(
100+
&rbacv1.Role{
101+
TypeMeta: metav1.TypeMeta{
102+
Kind: "Role",
103+
APIVersion: rbacv1.SchemeGroupVersion.String(),
104+
},
105+
ObjectMeta: metav1.ObjectMeta{
106+
Namespace: namespace,
107+
Name: name,
108+
},
109+
},
110+
).(*rbacv1.Role)
111+
}
112+
113+
// CreateClusterRoleResource creates a ClusterRole resource with name 'name' and applying any
114+
// ClusterRole related options in opts
115+
func CreateClusterRoleResource(name string, opts ...ResourceCreatorOption) *rbacv1.ClusterRole {
116+
return ResourceCreatorOptions(opts).ApplyTo(
117+
&rbacv1.ClusterRole{
118+
TypeMeta: metav1.TypeMeta{
119+
Kind: "ClusterRole",
120+
APIVersion: rbacv1.SchemeGroupVersion.String(),
121+
},
122+
ObjectMeta: metav1.ObjectMeta{
123+
Name: name,
124+
},
125+
},
126+
).(*rbacv1.ClusterRole)
127+
}
128+
129+
// CreateClusterRoleBindingResource creates a ClusterRoleBinding resource with name 'name' and applying any
130+
// ClusterRoleBinding related options in opts
131+
func CreateClusterRoleBindingResource(name string, opts ...ResourceCreatorOption) *rbacv1.ClusterRoleBinding {
132+
return ResourceCreatorOptions(opts).ApplyTo(
133+
&rbacv1.ClusterRoleBinding{
134+
TypeMeta: metav1.TypeMeta{
135+
Kind: "ClusterRoleBinding",
136+
APIVersion: rbacv1.SchemeGroupVersion.String(),
137+
},
138+
ObjectMeta: metav1.ObjectMeta{
139+
Name: name,
140+
},
141+
},
142+
).(*rbacv1.ClusterRoleBinding)
143+
}
144+
145+
// CreateRoleBindingResource creates a RoleBinding resource with name 'name', namespace 'namespace', and applying any
146+
// RoleBinding related options in opts
147+
func CreateRoleBindingResource(name string, namespace string, opts ...ResourceCreatorOption) *rbacv1.RoleBinding {
148+
return ResourceCreatorOptions(opts).ApplyTo(
149+
&rbacv1.RoleBinding{
150+
TypeMeta: metav1.TypeMeta{
151+
Kind: "RoleBinding",
152+
APIVersion: rbacv1.SchemeGroupVersion.String(),
153+
},
154+
ObjectMeta: metav1.ObjectMeta{
155+
Namespace: namespace,
156+
Name: name,
157+
},
158+
},
159+
).(*rbacv1.RoleBinding)
160+
}
161+
162+
// CreateDeploymentResource creates a Deployment resource with name 'name', namespace 'namespace', and applying any
163+
// Deployment related options in opts
164+
func CreateDeploymentResource(name string, namespace string, opts ...ResourceCreatorOption) *appsv1.Deployment {
165+
return ResourceCreatorOptions(opts).ApplyTo(
166+
&appsv1.Deployment{
167+
TypeMeta: metav1.TypeMeta{
168+
Kind: "Deployment",
169+
APIVersion: appsv1.SchemeGroupVersion.String(),
170+
},
171+
ObjectMeta: metav1.ObjectMeta{
172+
Namespace: namespace,
173+
Name: name,
174+
},
175+
},
176+
).(*appsv1.Deployment)
177+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package generators_test
2+
3+
import (
4+
"maps"
5+
"slices"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
corev1 "k8s.io/api/core/v1"
11+
rbacv1 "k8s.io/api/rbac/v1"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
14+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/generators"
15+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
16+
)
17+
18+
func Test_OptionsApplyToExecutesIgnoresNil(t *testing.T) {
19+
opts := []generators.ResourceCreatorOption{
20+
func(object client.Object) {
21+
object.SetAnnotations(util.MergeMaps(object.GetAnnotations(), map[string]string{"h": ""}))
22+
},
23+
nil,
24+
func(object client.Object) {
25+
object.SetAnnotations(util.MergeMaps(object.GetAnnotations(), map[string]string{"i": ""}))
26+
},
27+
nil,
28+
}
29+
30+
require.Nil(t, generators.ResourceCreatorOptions(nil).ApplyTo(nil))
31+
require.Nil(t, generators.ResourceCreatorOptions([]generators.ResourceCreatorOption{}).ApplyTo(nil))
32+
33+
obj := generators.ResourceCreatorOptions(opts).ApplyTo(&corev1.ConfigMap{})
34+
require.Equal(t, "hi", strings.Join(slices.Sorted(maps.Keys(obj.GetAnnotations())), ""))
35+
}
36+
37+
func Test_CreateServiceAccount(t *testing.T) {
38+
svc := generators.CreateServiceAccountResource("my-sa", "my-namespace")
39+
require.NotNil(t, svc)
40+
require.Equal(t, "my-sa", svc.Name)
41+
require.Equal(t, "my-namespace", svc.Namespace)
42+
}
43+
44+
func Test_CreateRole(t *testing.T) {
45+
role := generators.CreateRoleResource("my-role", "my-namespace")
46+
require.NotNil(t, role)
47+
require.Equal(t, "my-role", role.Name)
48+
require.Equal(t, "my-namespace", role.Namespace)
49+
}
50+
51+
func Test_CreateRoleBinding(t *testing.T) {
52+
roleBinding := generators.CreateRoleBindingResource("my-role-binding", "my-namespace")
53+
require.NotNil(t, roleBinding)
54+
require.Equal(t, "my-role-binding", roleBinding.Name)
55+
require.Equal(t, "my-namespace", roleBinding.Namespace)
56+
}
57+
58+
func Test_CreateClusterRole(t *testing.T) {
59+
clusterRole := generators.CreateClusterRoleResource("my-cluster-role")
60+
require.NotNil(t, clusterRole)
61+
require.Equal(t, "my-cluster-role", clusterRole.Name)
62+
}
63+
64+
func Test_CreateClusterRoleBinding(t *testing.T) {
65+
clusterRoleBinding := generators.CreateClusterRoleBindingResource("my-cluster-role-binding")
66+
require.NotNil(t, clusterRoleBinding)
67+
require.Equal(t, "my-cluster-role-binding", clusterRoleBinding.Name)
68+
}
69+
70+
func Test_CreateDeployment(t *testing.T) {
71+
deployment := generators.CreateDeploymentResource("my-deployment", "my-namespace")
72+
require.NotNil(t, deployment)
73+
require.Equal(t, "my-deployment", deployment.Name)
74+
require.Equal(t, "my-namespace", deployment.Namespace)
75+
}
76+
77+
func Test_WithSubjects(t *testing.T) {
78+
for _, tc := range []struct {
79+
name string
80+
subjects []rbacv1.Subject
81+
}{
82+
{
83+
name: "empty",
84+
subjects: []rbacv1.Subject{},
85+
}, {
86+
name: "nil",
87+
subjects: nil,
88+
}, {
89+
name: "single subject",
90+
subjects: []rbacv1.Subject{
91+
{
92+
APIGroup: rbacv1.GroupName,
93+
Kind: rbacv1.ServiceAccountKind,
94+
Name: "my-sa",
95+
Namespace: "my-namespace",
96+
},
97+
},
98+
}, {
99+
name: "multiple subjects",
100+
subjects: []rbacv1.Subject{
101+
{
102+
APIGroup: rbacv1.GroupName,
103+
Kind: rbacv1.ServiceAccountKind,
104+
Name: "my-sa",
105+
Namespace: "my-namespace",
106+
},
107+
},
108+
},
109+
} {
110+
t.Run(tc.name, func(t *testing.T) {
111+
roleBinding := generators.CreateRoleBindingResource("my-role", "my-namespace", generators.WithSubjects(tc.subjects...))
112+
require.NotNil(t, roleBinding)
113+
require.Equal(t, roleBinding.Subjects, tc.subjects)
114+
115+
clusterRoleBinding := generators.CreateClusterRoleBindingResource("my-role", generators.WithSubjects(tc.subjects...))
116+
require.NotNil(t, clusterRoleBinding)
117+
require.Equal(t, clusterRoleBinding.Subjects, tc.subjects)
118+
})
119+
}
120+
}
121+
122+
func Test_WithRules(t *testing.T) {
123+
for _, tc := range []struct {
124+
name string
125+
rules []rbacv1.PolicyRule
126+
}{
127+
{
128+
name: "empty",
129+
rules: []rbacv1.PolicyRule{},
130+
}, {
131+
name: "nil",
132+
rules: nil,
133+
}, {
134+
name: "single subject",
135+
rules: []rbacv1.PolicyRule{
136+
{
137+
Verbs: []string{"*"},
138+
APIGroups: []string{"*"},
139+
Resources: []string{"*"},
140+
},
141+
},
142+
}, {
143+
name: "multiple subjects",
144+
rules: []rbacv1.PolicyRule{
145+
{
146+
Verbs: []string{"*"},
147+
APIGroups: []string{"*"},
148+
Resources: []string{"*"},
149+
ResourceNames: []string{"my-resource"},
150+
}, {
151+
Verbs: []string{"get", "list", "watch"},
152+
APIGroups: []string{"appsv1"},
153+
Resources: []string{"deployments", "replicasets", "statefulsets"},
154+
},
155+
},
156+
},
157+
} {
158+
t.Run(tc.name, func(t *testing.T) {
159+
role := generators.CreateRoleResource("my-role", "my-namespace", generators.WithRules(tc.rules...))
160+
require.NotNil(t, role)
161+
require.Equal(t, role.Rules, tc.rules)
162+
163+
clusterRole := generators.CreateClusterRoleResource("my-role", generators.WithRules(tc.rules...))
164+
require.NotNil(t, clusterRole)
165+
require.Equal(t, clusterRole.Rules, tc.rules)
166+
})
167+
}
168+
}
169+
170+
func Test_WithRoleRef(t *testing.T) {
171+
roleRef := rbacv1.RoleRef{
172+
APIGroup: rbacv1.GroupName,
173+
Kind: "Role",
174+
Name: "my-role",
175+
}
176+
177+
roleBinding := generators.CreateRoleBindingResource("my-role-binding", "my-namespace", generators.WithRoleRef(roleRef))
178+
require.NotNil(t, roleBinding)
179+
require.Equal(t, roleRef, roleBinding.RoleRef)
180+
181+
clusterRoleBinding := generators.CreateClusterRoleBindingResource("my-cluster-role-binding", generators.WithRoleRef(roleRef))
182+
require.NotNil(t, clusterRoleBinding)
183+
require.Equal(t, roleRef, clusterRoleBinding.RoleRef)
184+
}
185+
186+
func Test_WithLabels(t *testing.T) {
187+
for _, tc := range []struct {
188+
name string
189+
labels map[string]string
190+
}{
191+
{
192+
name: "empty",
193+
labels: map[string]string{},
194+
}, {
195+
name: "nil",
196+
labels: nil,
197+
}, {
198+
name: "not empty",
199+
labels: map[string]string{
200+
"foo": "bar",
201+
},
202+
},
203+
} {
204+
t.Run(tc.name, func(t *testing.T) {
205+
dep := generators.CreateDeploymentResource("my-deployment", "my-namespace", generators.WithLabels(tc.labels))
206+
require.NotNil(t, dep)
207+
require.Equal(t, tc.labels, dep.Labels)
208+
})
209+
}
210+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package render
2+
3+
import (
4+
"errors"
5+
6+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
7+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8+
"sigs.k8s.io/controller-runtime/pkg/client"
9+
10+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
11+
12+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
13+
)
14+
15+
type RegistryV1 struct {
16+
PackageName string
17+
CSV v1alpha1.ClusterServiceVersion
18+
CRDs []apiextensionsv1.CustomResourceDefinition
19+
Others []unstructured.Unstructured
20+
}
21+
22+
// BundleValidator validates a RegistryV1 bundle by executing a series of
23+
// checks on it and collecting any errors that were found
24+
type BundleValidator []func(v1 *RegistryV1) []error
25+
26+
func (v BundleValidator) Validate(rv1 *RegistryV1) error {
27+
var errs []error
28+
for _, validator := range v {
29+
errs = append(errs, validator(rv1)...)
30+
}
31+
return errors.Join(errs...)
32+
}
33+
34+
// ResourceGenerator generates resources given a registry+v1 bundle and options
35+
type ResourceGenerator func(rv1 *RegistryV1, opts Options) ([]client.Object, error)
36+
37+
func (g ResourceGenerator) GenerateResources(rv1 *RegistryV1, opts Options) ([]client.Object, error) {
38+
return g(rv1, opts)
39+
}
40+
41+
// ResourceGenerators aggregates generators. Its GenerateResource method will call all of its generators and return
42+
// generated resources.
43+
type ResourceGenerators []ResourceGenerator
44+
45+
func (r ResourceGenerators) GenerateResources(rv1 *RegistryV1, opts Options) ([]client.Object, error) {
46+
//nolint:prealloc
47+
var renderedObjects []client.Object
48+
for _, generator := range r {
49+
objs, err := generator.GenerateResources(rv1, opts)
50+
if err != nil {
51+
return nil, err
52+
}
53+
renderedObjects = append(renderedObjects, objs...)
54+
}
55+
return renderedObjects, nil
56+
}
57+
58+
func (r ResourceGenerators) ResourceGenerator() ResourceGenerator {
59+
return r.GenerateResources
60+
}
61+
62+
type UniqueNameGenerator func(string, interface{}) (string, error)
63+
64+
type Options struct {
65+
InstallNamespace string
66+
TargetNamespaces []string
67+
UniqueNameGenerator UniqueNameGenerator
68+
}
69+
70+
func (o *Options) apply(opts ...Option) *Options {
71+
for _, opt := range opts {
72+
opt(o)
73+
}
74+
return o
75+
}
76+
77+
type Option func(*Options)
78+
79+
type BundleRenderer struct {
80+
BundleValidator BundleValidator
81+
ResourceGenerators []ResourceGenerator
82+
}
83+
84+
func (r BundleRenderer) Render(rv1 RegistryV1, installNamespace string, watchNamespaces []string, opts ...Option) ([]client.Object, error) {
85+
// validate bundle
86+
if err := r.BundleValidator.Validate(&rv1); err != nil {
87+
return nil, err
88+
}
89+
90+
genOpts := (&Options{
91+
InstallNamespace: installNamespace,
92+
TargetNamespaces: watchNamespaces,
93+
UniqueNameGenerator: DefaultUniqueNameGenerator,
94+
}).apply(opts...)
95+
96+
// generate bundle objects
97+
objs, err := ResourceGenerators(r.ResourceGenerators).GenerateResources(&rv1, *genOpts)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
return objs, nil
103+
}
104+
105+
func DefaultUniqueNameGenerator(base string, o interface{}) (string, error) {
106+
hashStr, err := util.DeepHashObject(o)
107+
if err != nil {
108+
return "", err
109+
}
110+
return util.ObjectNameForBaseAndSuffix(base, hashStr), nil
111+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package render_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"reflect"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
appsv1 "k8s.io/api/apps/v1"
11+
corev1 "k8s.io/api/core/v1"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
14+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
15+
)
16+
17+
func Test_BundleRenderer_NoConfig(t *testing.T) {
18+
renderer := render.BundleRenderer{}
19+
objs, err := renderer.Render(render.RegistryV1{}, "", nil)
20+
require.NoError(t, err)
21+
require.Empty(t, objs)
22+
}
23+
24+
func Test_BundleRenderer_ValidatesBundle(t *testing.T) {
25+
renderer := render.BundleRenderer{
26+
BundleValidator: render.BundleValidator{
27+
func(v1 *render.RegistryV1) []error {
28+
return []error{errors.New("this bundle is invalid")}
29+
},
30+
},
31+
}
32+
objs, err := renderer.Render(render.RegistryV1{}, "", nil)
33+
require.Nil(t, objs)
34+
require.Error(t, err)
35+
require.Contains(t, err.Error(), "this bundle is invalid")
36+
}
37+
38+
func Test_BundleRenderer_CreatesCorrectDefaultOptions(t *testing.T) {
39+
expectedInstallNamespace := "install-namespace"
40+
expectedTargetNamespaces := []string{"ns-one", "ns-two"}
41+
expectedUniqueNameGenerator := render.DefaultUniqueNameGenerator
42+
43+
renderer := render.BundleRenderer{
44+
ResourceGenerators: []render.ResourceGenerator{
45+
func(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
46+
require.Equal(t, expectedInstallNamespace, opts.InstallNamespace)
47+
require.Equal(t, expectedTargetNamespaces, opts.TargetNamespaces)
48+
require.Equal(t, reflect.ValueOf(expectedUniqueNameGenerator).Pointer(), reflect.ValueOf(render.DefaultUniqueNameGenerator).Pointer(), "options has unexpected default unique name generator")
49+
return nil, nil
50+
},
51+
},
52+
}
53+
54+
_, _ = renderer.Render(render.RegistryV1{}, expectedInstallNamespace, expectedTargetNamespaces)
55+
}
56+
57+
func Test_BundleRenderer_CallsResourceGenerators(t *testing.T) {
58+
renderer := render.BundleRenderer{
59+
ResourceGenerators: []render.ResourceGenerator{
60+
func(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
61+
return []client.Object{&corev1.Namespace{}, &corev1.Service{}}, nil
62+
},
63+
func(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
64+
return []client.Object{&appsv1.Deployment{}}, nil
65+
},
66+
},
67+
}
68+
objs, err := renderer.Render(render.RegistryV1{}, "", nil)
69+
require.NoError(t, err)
70+
require.Equal(t, []client.Object{&corev1.Namespace{}, &corev1.Service{}, &appsv1.Deployment{}}, objs)
71+
}
72+
73+
func Test_BundleRenderer_ReturnsResourceGeneratorErrors(t *testing.T) {
74+
renderer := render.BundleRenderer{
75+
ResourceGenerators: []render.ResourceGenerator{
76+
func(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
77+
return []client.Object{&corev1.Namespace{}, &corev1.Service{}}, nil
78+
},
79+
func(rv1 *render.RegistryV1, opts render.Options) ([]client.Object, error) {
80+
return nil, fmt.Errorf("generator error")
81+
},
82+
},
83+
}
84+
objs, err := renderer.Render(render.RegistryV1{}, "", nil)
85+
require.Nil(t, objs)
86+
require.Error(t, err)
87+
require.Contains(t, err.Error(), "generator error")
88+
}
89+
90+
func Test_BundleValidatorCallsAllValidationFnsInOrder(t *testing.T) {
91+
actual := ""
92+
val := render.BundleValidator{
93+
func(v1 *render.RegistryV1) []error {
94+
actual += "h"
95+
return nil
96+
},
97+
func(v1 *render.RegistryV1) []error {
98+
actual += "i"
99+
return nil
100+
},
101+
}
102+
require.NoError(t, val.Validate(nil))
103+
require.Equal(t, "hi", actual)
104+
}

‎internal/operator-controller/rukpak/convert/validator.go renamed to ‎internal/operator-controller/rukpak/render/validators/validator.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
1-
package convert
1+
package validators
22

33
import (
44
"errors"
55
"fmt"
66
"slices"
77

88
"k8s.io/apimachinery/pkg/util/sets"
9-
)
10-
11-
type BundleValidator []func(v1 *RegistryV1) []error
129

13-
func (v BundleValidator) Validate(rv1 *RegistryV1) error {
14-
var errs []error
15-
for _, validator := range v {
16-
errs = append(errs, validator(rv1)...)
17-
}
18-
return errors.Join(errs...)
19-
}
10+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
11+
)
2012

21-
var RegistryV1BundleValidator = BundleValidator{
13+
// RegistryV1BundleValidator validates RegistryV1 bundles
14+
var RegistryV1BundleValidator = render.BundleValidator{
2215
// NOTE: if you update this list, Test_BundleValidatorHasAllValidationFns will fail until
2316
// you bring the same changes over to that test. This helps ensure all validation rules are executed
2417
// while giving us the flexibility to test each validation function individually
@@ -30,7 +23,7 @@ var RegistryV1BundleValidator = BundleValidator{
3023

3124
// CheckDeploymentSpecUniqueness checks that each strategy deployment spec in the csv has a unique name.
3225
// Errors are sorted by deployment name.
33-
func CheckDeploymentSpecUniqueness(rv1 *RegistryV1) []error {
26+
func CheckDeploymentSpecUniqueness(rv1 *render.RegistryV1) []error {
3427
deploymentNameSet := sets.Set[string]{}
3528
duplicateDeploymentNames := sets.Set[string]{}
3629
for _, dep := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs {
@@ -48,7 +41,7 @@ func CheckDeploymentSpecUniqueness(rv1 *RegistryV1) []error {
4841
}
4942

5043
// CheckOwnedCRDExistence checks bundle owned custom resource definitions declared in the csv exist in the bundle
51-
func CheckOwnedCRDExistence(rv1 *RegistryV1) []error {
44+
func CheckOwnedCRDExistence(rv1 *render.RegistryV1) []error {
5245
crdsNames := sets.Set[string]{}
5346
for _, crd := range rv1.CRDs {
5447
crdsNames.Insert(crd.Name)
@@ -69,7 +62,7 @@ func CheckOwnedCRDExistence(rv1 *RegistryV1) []error {
6962
}
7063

7164
// CheckCRDResourceUniqueness checks that the bundle CRD names are unique
72-
func CheckCRDResourceUniqueness(rv1 *RegistryV1) []error {
65+
func CheckCRDResourceUniqueness(rv1 *render.RegistryV1) []error {
7366
crdsNames := sets.Set[string]{}
7467
duplicateCRDNames := sets.Set[string]{}
7568
for _, crd := range rv1.CRDs {
@@ -87,7 +80,7 @@ func CheckCRDResourceUniqueness(rv1 *RegistryV1) []error {
8780
}
8881

8982
// CheckPackageNameNotEmpty checks that PackageName is not empty
90-
func CheckPackageNameNotEmpty(rv1 *RegistryV1) []error {
83+
func CheckPackageNameNotEmpty(rv1 *render.RegistryV1) []error {
9184
if rv1.PackageName == "" {
9285
return []error{errors.New("package name is empty")}
9386
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package convert_test
1+
package validators_test
22

33
import (
44
"errors"
@@ -11,51 +11,37 @@ import (
1111

1212
"github.com/operator-framework/api/pkg/operators/v1alpha1"
1313

14-
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert"
14+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
15+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/validators"
16+
. "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
1517
)
1618

1719
func Test_BundleValidatorHasAllValidationFns(t *testing.T) {
18-
expectedValidationFns := []func(v1 *convert.RegistryV1) []error{
19-
convert.CheckDeploymentSpecUniqueness,
20-
convert.CheckCRDResourceUniqueness,
21-
convert.CheckOwnedCRDExistence,
22-
convert.CheckPackageNameNotEmpty,
20+
expectedValidationFns := []func(v1 *render.RegistryV1) []error{
21+
validators.CheckDeploymentSpecUniqueness,
22+
validators.CheckCRDResourceUniqueness,
23+
validators.CheckOwnedCRDExistence,
24+
validators.CheckPackageNameNotEmpty,
2325
}
24-
actualValidationFns := convert.RegistryV1BundleValidator
26+
actualValidationFns := validators.RegistryV1BundleValidator
2527

2628
require.Equal(t, len(expectedValidationFns), len(actualValidationFns))
2729
for i := range expectedValidationFns {
2830
require.Equal(t, reflect.ValueOf(expectedValidationFns[i]).Pointer(), reflect.ValueOf(actualValidationFns[i]).Pointer(), "bundle validator has unexpected validation function")
2931
}
3032
}
3133

32-
func Test_BundleValidatorCallsAllValidationFnsInOrder(t *testing.T) {
33-
actual := ""
34-
validator := convert.BundleValidator{
35-
func(v1 *convert.RegistryV1) []error {
36-
actual += "h"
37-
return nil
38-
},
39-
func(v1 *convert.RegistryV1) []error {
40-
actual += "i"
41-
return nil
42-
},
43-
}
44-
require.NoError(t, validator.Validate(nil))
45-
require.Equal(t, "hi", actual)
46-
}
47-
4834
func Test_CheckDeploymentSpecUniqueness(t *testing.T) {
4935
for _, tc := range []struct {
5036
name string
51-
bundle *convert.RegistryV1
37+
bundle *render.RegistryV1
5238
expectedErrs []error
5339
}{
5440
{
5541
name: "accepts bundles with unique deployment strategy spec names",
56-
bundle: &convert.RegistryV1{
57-
CSV: makeCSV(
58-
withStrategyDeploymentSpecs(
42+
bundle: &render.RegistryV1{
43+
CSV: MakeCSV(
44+
WithStrategyDeploymentSpecs(
5945
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"},
6046
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-two"},
6147
),
@@ -64,9 +50,9 @@ func Test_CheckDeploymentSpecUniqueness(t *testing.T) {
6450
expectedErrs: []error{},
6551
}, {
6652
name: "rejects bundles with duplicate deployment strategy spec names",
67-
bundle: &convert.RegistryV1{
68-
CSV: makeCSV(
69-
withStrategyDeploymentSpecs(
53+
bundle: &render.RegistryV1{
54+
CSV: MakeCSV(
55+
WithStrategyDeploymentSpecs(
7056
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"},
7157
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-two"},
7258
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-one"},
@@ -78,9 +64,9 @@ func Test_CheckDeploymentSpecUniqueness(t *testing.T) {
7864
},
7965
}, {
8066
name: "errors are ordered by deployment strategy spec name",
81-
bundle: &convert.RegistryV1{
82-
CSV: makeCSV(
83-
withStrategyDeploymentSpecs(
67+
bundle: &render.RegistryV1{
68+
CSV: MakeCSV(
69+
WithStrategyDeploymentSpecs(
8470
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-a"},
8571
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-b"},
8672
v1alpha1.StrategyDeploymentSpec{Name: "test-deployment-c"},
@@ -96,7 +82,7 @@ func Test_CheckDeploymentSpecUniqueness(t *testing.T) {
9682
},
9783
} {
9884
t.Run(tc.name, func(t *testing.T) {
99-
errs := convert.CheckDeploymentSpecUniqueness(tc.bundle)
85+
errs := validators.CheckDeploymentSpecUniqueness(tc.bundle)
10086
require.Equal(t, tc.expectedErrs, errs)
10187
})
10288
}
@@ -105,12 +91,12 @@ func Test_CheckDeploymentSpecUniqueness(t *testing.T) {
10591
func Test_CRDResourceUniqueness(t *testing.T) {
10692
for _, tc := range []struct {
10793
name string
108-
bundle *convert.RegistryV1
94+
bundle *render.RegistryV1
10995
expectedErrs []error
11096
}{
11197
{
11298
name: "accepts bundles with unique custom resource definition resources",
113-
bundle: &convert.RegistryV1{
99+
bundle: &render.RegistryV1{
114100
CRDs: []apiextensionsv1.CustomResourceDefinition{
115101
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
116102
{ObjectMeta: metav1.ObjectMeta{Name: "b.crd.something"}},
@@ -119,7 +105,7 @@ func Test_CRDResourceUniqueness(t *testing.T) {
119105
expectedErrs: []error{},
120106
}, {
121107
name: "rejects bundles with duplicate custom resource definition resources",
122-
bundle: &convert.RegistryV1{CRDs: []apiextensionsv1.CustomResourceDefinition{
108+
bundle: &render.RegistryV1{CRDs: []apiextensionsv1.CustomResourceDefinition{
123109
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
124110
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
125111
}},
@@ -128,7 +114,7 @@ func Test_CRDResourceUniqueness(t *testing.T) {
128114
},
129115
}, {
130116
name: "errors are ordered by custom resource definition name",
131-
bundle: &convert.RegistryV1{CRDs: []apiextensionsv1.CustomResourceDefinition{
117+
bundle: &render.RegistryV1{CRDs: []apiextensionsv1.CustomResourceDefinition{
132118
{ObjectMeta: metav1.ObjectMeta{Name: "c.crd.something"}},
133119
{ObjectMeta: metav1.ObjectMeta{Name: "c.crd.something"}},
134120
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
@@ -141,7 +127,7 @@ func Test_CRDResourceUniqueness(t *testing.T) {
141127
},
142128
} {
143129
t.Run(tc.name, func(t *testing.T) {
144-
err := convert.CheckCRDResourceUniqueness(tc.bundle)
130+
err := validators.CheckCRDResourceUniqueness(tc.bundle)
145131
require.Equal(t, tc.expectedErrs, err)
146132
})
147133
}
@@ -150,18 +136,18 @@ func Test_CRDResourceUniqueness(t *testing.T) {
150136
func Test_CheckOwnedCRDExistence(t *testing.T) {
151137
for _, tc := range []struct {
152138
name string
153-
bundle *convert.RegistryV1
139+
bundle *render.RegistryV1
154140
expectedErrs []error
155141
}{
156142
{
157143
name: "accepts bundles with existing owned custom resource definition resources",
158-
bundle: &convert.RegistryV1{
144+
bundle: &render.RegistryV1{
159145
CRDs: []apiextensionsv1.CustomResourceDefinition{
160146
{ObjectMeta: metav1.ObjectMeta{Name: "a.crd.something"}},
161147
{ObjectMeta: metav1.ObjectMeta{Name: "b.crd.something"}},
162148
},
163-
CSV: makeCSV(
164-
withOwnedCRDs(
149+
CSV: MakeCSV(
150+
WithOwnedCRDs(
165151
v1alpha1.CRDDescription{Name: "a.crd.something"},
166152
v1alpha1.CRDDescription{Name: "b.crd.something"},
167153
),
@@ -170,21 +156,21 @@ func Test_CheckOwnedCRDExistence(t *testing.T) {
170156
expectedErrs: []error{},
171157
}, {
172158
name: "rejects bundles with missing owned custom resource definition resources",
173-
bundle: &convert.RegistryV1{
159+
bundle: &render.RegistryV1{
174160
CRDs: []apiextensionsv1.CustomResourceDefinition{},
175-
CSV: makeCSV(
176-
withOwnedCRDs(v1alpha1.CRDDescription{Name: "a.crd.something"}),
161+
CSV: MakeCSV(
162+
WithOwnedCRDs(v1alpha1.CRDDescription{Name: "a.crd.something"}),
177163
),
178164
},
179165
expectedErrs: []error{
180166
errors.New("cluster service definition references owned custom resource definition 'a.crd.something' not found in bundle"),
181167
},
182168
}, {
183169
name: "errors are ordered by owned custom resource definition name",
184-
bundle: &convert.RegistryV1{
170+
bundle: &render.RegistryV1{
185171
CRDs: []apiextensionsv1.CustomResourceDefinition{},
186-
CSV: makeCSV(
187-
withOwnedCRDs(
172+
CSV: MakeCSV(
173+
WithOwnedCRDs(
188174
v1alpha1.CRDDescription{Name: "a.crd.something"},
189175
v1alpha1.CRDDescription{Name: "c.crd.something"},
190176
v1alpha1.CRDDescription{Name: "b.crd.something"},
@@ -199,7 +185,7 @@ func Test_CheckOwnedCRDExistence(t *testing.T) {
199185
},
200186
} {
201187
t.Run(tc.name, func(t *testing.T) {
202-
errs := convert.CheckOwnedCRDExistence(tc.bundle)
188+
errs := validators.CheckOwnedCRDExistence(tc.bundle)
203189
require.Equal(t, tc.expectedErrs, errs)
204190
})
205191
}
@@ -208,47 +194,25 @@ func Test_CheckOwnedCRDExistence(t *testing.T) {
208194
func Test_CheckPackageNameNotEmpty(t *testing.T) {
209195
for _, tc := range []struct {
210196
name string
211-
bundle *convert.RegistryV1
197+
bundle *render.RegistryV1
212198
expectedErrs []error
213199
}{
214200
{
215201
name: "accepts bundles with non-empty package name",
216-
bundle: &convert.RegistryV1{
202+
bundle: &render.RegistryV1{
217203
PackageName: "not-empty",
218204
},
219205
}, {
220206
name: "rejects bundles with empty package name",
221-
bundle: &convert.RegistryV1{},
207+
bundle: &render.RegistryV1{},
222208
expectedErrs: []error{
223209
errors.New("package name is empty"),
224210
},
225211
},
226212
} {
227213
t.Run(tc.name, func(t *testing.T) {
228-
errs := convert.CheckPackageNameNotEmpty(tc.bundle)
214+
errs := validators.CheckPackageNameNotEmpty(tc.bundle)
229215
require.Equal(t, tc.expectedErrs, errs)
230216
})
231217
}
232218
}
233-
234-
type csvOption func(version *v1alpha1.ClusterServiceVersion)
235-
236-
func withStrategyDeploymentSpecs(strategyDeploymentSpecs ...v1alpha1.StrategyDeploymentSpec) csvOption {
237-
return func(csv *v1alpha1.ClusterServiceVersion) {
238-
csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs = strategyDeploymentSpecs
239-
}
240-
}
241-
242-
func withOwnedCRDs(crdDesc ...v1alpha1.CRDDescription) csvOption {
243-
return func(csv *v1alpha1.ClusterServiceVersion) {
244-
csv.Spec.CustomResourceDefinitions.Owned = crdDesc
245-
}
246-
}
247-
248-
func makeCSV(opts ...csvOption) v1alpha1.ClusterServiceVersion {
249-
csv := v1alpha1.ClusterServiceVersion{}
250-
for _, opt := range opts {
251-
opt(&csv)
252-
}
253-
return csv
254-
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package util
2+
3+
import (
4+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
6+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
7+
)
8+
9+
type CSVOption func(version *v1alpha1.ClusterServiceVersion)
10+
11+
//nolint:unparam
12+
func WithName(name string) CSVOption {
13+
return func(csv *v1alpha1.ClusterServiceVersion) {
14+
csv.Name = name
15+
}
16+
}
17+
18+
func WithStrategyDeploymentSpecs(strategyDeploymentSpecs ...v1alpha1.StrategyDeploymentSpec) CSVOption {
19+
return func(csv *v1alpha1.ClusterServiceVersion) {
20+
csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs = strategyDeploymentSpecs
21+
}
22+
}
23+
24+
func WithAnnotations(annotations map[string]string) CSVOption {
25+
return func(csv *v1alpha1.ClusterServiceVersion) {
26+
csv.Annotations = annotations
27+
}
28+
}
29+
30+
func WithPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) CSVOption {
31+
return func(csv *v1alpha1.ClusterServiceVersion) {
32+
csv.Spec.InstallStrategy.StrategySpec.Permissions = permissions
33+
}
34+
}
35+
36+
func WithClusterPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) CSVOption {
37+
return func(csv *v1alpha1.ClusterServiceVersion) {
38+
csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions = permissions
39+
}
40+
}
41+
42+
func WithOwnedCRDs(crdDesc ...v1alpha1.CRDDescription) CSVOption {
43+
return func(csv *v1alpha1.ClusterServiceVersion) {
44+
csv.Spec.CustomResourceDefinitions.Owned = crdDesc
45+
}
46+
}
47+
48+
func MakeCSV(opts ...CSVOption) v1alpha1.ClusterServiceVersion {
49+
csv := v1alpha1.ClusterServiceVersion{
50+
TypeMeta: metav1.TypeMeta{
51+
APIVersion: v1alpha1.SchemeGroupVersion.String(),
52+
Kind: "ClusterServiceVersion",
53+
},
54+
}
55+
for _, opt := range opts {
56+
opt(&csv)
57+
}
58+
return csv
59+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package util
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
rbacv1 "k8s.io/api/rbac/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
10+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
11+
)
12+
13+
func Test_MakeCSV(t *testing.T) {
14+
csv := MakeCSV()
15+
require.Equal(t, v1alpha1.ClusterServiceVersion{
16+
TypeMeta: metav1.TypeMeta{
17+
Kind: "ClusterServiceVersion",
18+
APIVersion: v1alpha1.SchemeGroupVersion.String(),
19+
},
20+
}, csv)
21+
}
22+
23+
func Test_MakeCSV_WithName(t *testing.T) {
24+
csv := MakeCSV(WithName("some-name"))
25+
require.Equal(t, v1alpha1.ClusterServiceVersion{
26+
TypeMeta: metav1.TypeMeta{
27+
Kind: "ClusterServiceVersion",
28+
APIVersion: v1alpha1.SchemeGroupVersion.String(),
29+
},
30+
ObjectMeta: metav1.ObjectMeta{
31+
Name: "some-name",
32+
},
33+
}, csv)
34+
}
35+
36+
func Test_MakeCSV_WithStrategyDeploymentSpecs(t *testing.T) {
37+
csv := MakeCSV(
38+
WithStrategyDeploymentSpecs(
39+
v1alpha1.StrategyDeploymentSpec{
40+
Name: "spec-one",
41+
},
42+
v1alpha1.StrategyDeploymentSpec{
43+
Name: "spec-two",
44+
},
45+
),
46+
)
47+
48+
require.Equal(t, v1alpha1.ClusterServiceVersion{
49+
TypeMeta: metav1.TypeMeta{
50+
Kind: "ClusterServiceVersion",
51+
APIVersion: v1alpha1.SchemeGroupVersion.String(),
52+
},
53+
Spec: v1alpha1.ClusterServiceVersionSpec{
54+
InstallStrategy: v1alpha1.NamedInstallStrategy{
55+
StrategySpec: v1alpha1.StrategyDetailsDeployment{
56+
DeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{
57+
{
58+
Name: "spec-one",
59+
},
60+
{
61+
Name: "spec-two",
62+
},
63+
},
64+
},
65+
},
66+
},
67+
}, csv)
68+
}
69+
70+
func Test_MakeCSV_WithPermissions(t *testing.T) {
71+
csv := MakeCSV(
72+
WithPermissions(
73+
v1alpha1.StrategyDeploymentPermissions{
74+
ServiceAccountName: "service-account",
75+
Rules: []rbacv1.PolicyRule{
76+
{
77+
APIGroups: []string{""},
78+
Resources: []string{"secrets"},
79+
Verbs: []string{"list", "watch"},
80+
},
81+
},
82+
},
83+
v1alpha1.StrategyDeploymentPermissions{
84+
ServiceAccountName: "",
85+
},
86+
),
87+
)
88+
89+
require.Equal(t, v1alpha1.ClusterServiceVersion{
90+
TypeMeta: metav1.TypeMeta{
91+
Kind: "ClusterServiceVersion",
92+
APIVersion: v1alpha1.SchemeGroupVersion.String(),
93+
},
94+
Spec: v1alpha1.ClusterServiceVersionSpec{
95+
InstallStrategy: v1alpha1.NamedInstallStrategy{
96+
StrategySpec: v1alpha1.StrategyDetailsDeployment{
97+
Permissions: []v1alpha1.StrategyDeploymentPermissions{
98+
{
99+
ServiceAccountName: "service-account",
100+
Rules: []rbacv1.PolicyRule{
101+
{
102+
APIGroups: []string{""},
103+
Resources: []string{"secrets"},
104+
Verbs: []string{"list", "watch"},
105+
},
106+
},
107+
},
108+
{
109+
ServiceAccountName: "",
110+
},
111+
},
112+
},
113+
},
114+
},
115+
}, csv)
116+
}
117+
118+
func Test_MakeCSV_WithClusterPermissions(t *testing.T) {
119+
csv := MakeCSV(
120+
WithClusterPermissions(
121+
v1alpha1.StrategyDeploymentPermissions{
122+
ServiceAccountName: "service-account",
123+
Rules: []rbacv1.PolicyRule{
124+
{
125+
APIGroups: []string{""},
126+
Resources: []string{"secrets"},
127+
Verbs: []string{"list", "watch"},
128+
},
129+
},
130+
},
131+
v1alpha1.StrategyDeploymentPermissions{
132+
ServiceAccountName: "",
133+
},
134+
),
135+
)
136+
137+
require.Equal(t, v1alpha1.ClusterServiceVersion{
138+
TypeMeta: metav1.TypeMeta{
139+
Kind: "ClusterServiceVersion",
140+
APIVersion: v1alpha1.SchemeGroupVersion.String(),
141+
},
142+
Spec: v1alpha1.ClusterServiceVersionSpec{
143+
InstallStrategy: v1alpha1.NamedInstallStrategy{
144+
StrategySpec: v1alpha1.StrategyDetailsDeployment{
145+
ClusterPermissions: []v1alpha1.StrategyDeploymentPermissions{
146+
{
147+
ServiceAccountName: "service-account",
148+
Rules: []rbacv1.PolicyRule{
149+
{
150+
APIGroups: []string{""},
151+
Resources: []string{"secrets"},
152+
Verbs: []string{"list", "watch"},
153+
},
154+
},
155+
},
156+
{
157+
ServiceAccountName: "",
158+
},
159+
},
160+
},
161+
},
162+
},
163+
}, csv)
164+
}
165+
166+
func Test_MakeCSV_WithOwnedCRDs(t *testing.T) {
167+
csv := MakeCSV(
168+
WithOwnedCRDs(
169+
v1alpha1.CRDDescription{Name: "a.crd.something"},
170+
v1alpha1.CRDDescription{Name: "b.crd.something"},
171+
),
172+
)
173+
174+
require.Equal(t, v1alpha1.ClusterServiceVersion{
175+
TypeMeta: metav1.TypeMeta{
176+
Kind: "ClusterServiceVersion",
177+
APIVersion: v1alpha1.SchemeGroupVersion.String(),
178+
},
179+
Spec: v1alpha1.ClusterServiceVersionSpec{
180+
CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{
181+
Owned: []v1alpha1.CRDDescription{
182+
{Name: "a.crd.something"},
183+
{Name: "b.crd.something"},
184+
},
185+
},
186+
},
187+
}, csv)
188+
}

‎internal/operator-controller/rukpak/util/util.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
package util
22

33
import (
4+
"fmt"
45
"io"
56

67
"k8s.io/cli-runtime/pkg/resource"
78
"sigs.k8s.io/controller-runtime/pkg/client"
89
)
910

11+
const maxNameLength = 63
12+
13+
func ObjectNameForBaseAndSuffix(base string, suffix string) string {
14+
if len(base)+len(suffix) > maxNameLength {
15+
base = base[:maxNameLength-len(suffix)-1]
16+
}
17+
return fmt.Sprintf("%s-%s", base, suffix)
18+
}
19+
1020
func MergeMaps(maps ...map[string]string) map[string]string {
1121
out := map[string]string{}
1222
for _, m := range maps {

‎internal/operator-controller/rukpak/util/util_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import (
1616
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
1717
)
1818

19+
func Test_ObjectNameForBaseAndSuffix(t *testing.T) {
20+
name := util.ObjectNameForBaseAndSuffix("my.object.thing.has.a.really.really.really.really.really.long.name", "suffix")
21+
require.Len(t, name, 63)
22+
require.Equal(t, "my.object.thing.has.a.really.really.really.really.really-suffix", name)
23+
}
24+
1925
func TestMergeMaps(t *testing.T) {
2026
tests := []struct {
2127
name string

0 commit comments

Comments
 (0)
Please sign in to comment.