Skip to content

Commit cc0422d

Browse files
author
Eric Stroczynski
authored
generate bundle: add --extra-service-accounts flag to consider extra-Deployment SAs (#4826)
Signed-off-by: Eric Stroczynski <[email protected]>
1 parent 3842bd3 commit cc0422d

File tree

12 files changed

+488
-463
lines changed

12 files changed

+488
-463
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
entries:
2+
- description: >
3+
Added --extra-service-accounts flag to `generate bundle` to consider roles bound to
4+
service accounts not specified in the operator's Deployment.
5+
kind: addition
6+
- description: >
7+
`generate bundle` adds ClusterRoles bound by RoleBindings to a CSV's `.spec.permissions`,
8+
since these become namespace-scoped at runtime. They will also be added to `.spec.clusterPermissions`
9+
if bound by a ClusterRoleBinding.
10+
kind: change

internal/cmd/operator-sdk/generate/bundle/bundle.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,17 @@ func (c bundleCmd) runManifests() (err error) {
197197
}
198198

199199
csvGen := gencsv.Generator{
200-
OperatorName: c.packageName,
201-
Version: c.version,
202-
Collector: col,
203-
Annotations: metricsannotations.MakeBundleObjectAnnotations(c.layout),
200+
OperatorName: c.packageName,
201+
Version: c.version,
202+
Collector: col,
203+
Annotations: metricsannotations.MakeBundleObjectAnnotations(c.layout),
204+
ExtraServiceAccounts: c.extraServiceAccounts,
204205
}
205206
if err := csvGen.Generate(opts...); err != nil {
206207
return fmt.Errorf("error generating ClusterServiceVersion: %v", err)
207208
}
208209

209-
objs := genutil.GetManifestObjects(col)
210+
objs := genutil.GetManifestObjects(col, c.extraServiceAccounts)
210211
if c.stdout {
211212
if err := genutil.WriteObjects(stdout, objs...); err != nil {
212213
return err

internal/cmd/operator-sdk/generate/bundle/cmd.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type bundleCmd struct {
3838
crdsDir string
3939
stdout bool
4040
quiet bool
41+
// ServiceAccount names to consider outside of the operator's service account.
42+
extraServiceAccounts []string
4143

4244
// Metadata options.
4345
channels string
@@ -129,6 +131,9 @@ func (c *bundleCmd) addFlagsTo(fs *pflag.FlagSet) {
129131
"Directory containing kustomize bases in a \"bases\" dir and a kustomization.yaml for operator-framework manifests")
130132
fs.StringVar(&c.channels, "channels", "alpha", "A comma-separated list of channels the bundle belongs to")
131133
fs.StringVar(&c.defaultChannel, "default-channel", "", "The default channel for the bundle")
134+
fs.StringSliceVar(&c.extraServiceAccounts, "extra-service-accounts", nil,
135+
"Names of service accounts, outside of the operator's Deployment account, "+
136+
"that have bindings to {Cluster}Roles that should be added to the CSV")
132137
fs.BoolVar(&c.overwrite, "overwrite", true, "Overwrite the bundle's metadata and Dockerfile if they exist")
133138
fs.BoolVarP(&c.quiet, "quiet", "q", false, "Run in quiet mode")
134139
fs.BoolVar(&c.stdout, "stdout", false, "Write bundle manifest to stdout")

internal/cmd/operator-sdk/generate/internal/manifests.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
)
2323

2424
// GetManifestObjects returns all objects to be written to a manifests directory from collector.Manifests.
25-
func GetManifestObjects(c *collector.Manifests) (objs []client.Object) {
25+
func GetManifestObjects(c *collector.Manifests, extraSAs []string) (objs []client.Object) {
2626
// All CRDs passed in should be written.
2727
for i := range c.V1CustomResourceDefinitions {
2828
objs = append(objs, &c.V1CustomResourceDefinitions[i])
@@ -32,8 +32,20 @@ func GetManifestObjects(c *collector.Manifests) (objs []client.Object) {
3232
}
3333

3434
// All ServiceAccounts passed in should be written.
35+
saSet := make(map[string]struct{}, len(extraSAs))
36+
for _, saName := range extraSAs {
37+
saSet[saName] = struct{}{}
38+
}
3539
for i := range c.ServiceAccounts {
36-
objs = append(objs, &c.ServiceAccounts[i])
40+
sa := c.ServiceAccounts[i]
41+
saSet[sa.GetName()] = struct{}{}
42+
objs = append(objs, &sa)
43+
}
44+
extraSAs = make([]string, len(saSet))
45+
i := 0
46+
for saName := range saSet {
47+
extraSAs[i] = saName
48+
i++
3749
}
3850

3951
// All Services passed in should be written.
@@ -50,10 +62,8 @@ func GetManifestObjects(c *collector.Manifests) (objs []client.Object) {
5062
}
5163

5264
// RBAC objects that are not a part of the CSV should be written.
53-
_, roleObjs := c.SplitCSVPermissionsObjects()
54-
objs = append(objs, roleObjs...)
55-
_, clusterRoleObjs := c.SplitCSVClusterPermissionsObjects()
56-
objs = append(objs, clusterRoleObjs...)
65+
_, _, rbacObjs := c.SplitCSVPermissionsObjects(extraSAs)
66+
objs = append(objs, rbacObjs...)
5767

5868
removeNamespace(objs)
5969
return objs

internal/cmd/operator-sdk/generate/internal/manifests_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ var _ = Describe("GetManifestObjects", func() {
4747
{ObjectMeta: metav1.ObjectMeta{Namespace: "bar"}},
4848
},
4949
}
50-
objs := GetManifestObjects(&m)
50+
objs := GetManifestObjects(&m, nil)
5151
Expect(objs).To(HaveLen(len(m.Roles) + len(m.ClusterRoles) + len(m.ServiceAccounts) + len(m.V1CustomResourceDefinitions) + len(m.V1beta1CustomResourceDefinitions)))
5252
for _, obj := range objs {
5353
Expect(obj.GetNamespace()).To(BeEmpty())

internal/cmd/operator-sdk/generate/packagemanifests/packagemanifests.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ func (c packagemanifestsCmd) run() error {
207207
}
208208

209209
if c.updateObjects {
210-
objs := genutil.GetManifestObjects(col)
210+
// Extra ServiceAccounts not supported by this command.
211+
objs := genutil.GetManifestObjects(col, nil)
211212
if c.stdout {
212213
if err := genutil.WriteObjects(stdout, objs...); err != nil {
213214
return err

internal/generate/clusterserviceversion/clusterserviceversion.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ type Generator struct {
5353
Collector *collector.Manifests
5454
// Annotations are applied to the resulting CSV.
5555
Annotations map[string]string
56+
// ExtraServiceAccounts are ServiceAccount names to consider when matching
57+
// {Cluster}Roles to include in a CSV via their Bindings.
58+
ExtraServiceAccounts []string
5659

5760
// Func that returns the writer the generated CSV's bytes are written to.
5861
getWriter func() (io.Writer, error)
@@ -163,7 +166,7 @@ func (g *Generator) generate() (base *operatorsv1alpha1.ClusterServiceVersion, e
163166
base.Spec.Replaces = genutil.MakeCSVName(g.OperatorName, g.FromVersion)
164167
}
165168

166-
if err := ApplyTo(g.Collector, base); err != nil {
169+
if err := ApplyTo(g.Collector, base, g.ExtraServiceAccounts); err != nil {
167170
return nil, err
168171
}
169172

internal/generate/clusterserviceversion/clusterserviceversion_updaters.go

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,17 @@ import (
3030
corev1 "k8s.io/api/core/v1"
3131
rbacv1 "k8s.io/api/rbac/v1"
3232
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
3334

3435
"github.com/operator-framework/operator-sdk/internal/generate/collector"
3536
"github.com/operator-framework/operator-sdk/internal/util/k8sutil"
3637
)
3738

3839
// ApplyTo applies relevant manifests in c to csv, sorts the applied updates,
3940
// and validates the result.
40-
func ApplyTo(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion) error {
41+
func ApplyTo(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion, extraSAs []string) error {
4142
// Apply manifests to the CSV object.
42-
if err := apply(c, csv); err != nil {
43+
if err := apply(c, csv, extraSAs); err != nil {
4344
return err
4445
}
4546

@@ -50,12 +51,13 @@ func ApplyTo(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersio
5051
}
5152

5253
// apply applies relevant manifests in c to csv.
53-
func apply(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion) error {
54+
func apply(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion, extraSAs []string) error {
5455
strategy := getCSVInstallStrategy(csv)
5556
switch strategy.StrategyName {
5657
case operatorsv1alpha1.InstallStrategyNameDeployment:
57-
applyRoles(c, &strategy.StrategySpec)
58-
applyClusterRoles(c, &strategy.StrategySpec)
58+
inPerms, inCPerms, _ := c.SplitCSVPermissionsObjects(extraSAs)
59+
applyRoles(c, inPerms, &strategy.StrategySpec, extraSAs)
60+
applyClusterRoles(c, inCPerms, &strategy.StrategySpec, extraSAs)
5961
applyDeployments(c, &strategy.StrategySpec)
6062
}
6163
csv.Spec.InstallStrategy = strategy
@@ -82,34 +84,46 @@ const defaultServiceAccountName = "default"
8284

8385
// applyRoles applies Roles to strategy's permissions field by combining Roles bound to ServiceAccounts
8486
// into one set of permissions.
85-
func applyRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) { //nolint:dupl
86-
objs, _ := c.SplitCSVPermissionsObjects()
87-
roleSet := make(map[string]*rbacv1.Role)
87+
func applyRoles(c *collector.Manifests, objs []client.Object, strategy *operatorsv1alpha1.StrategyDetailsDeployment, extraSAs []string) { //nolint:dupl
88+
roleSet := make(map[string]rbacv1.Role)
89+
cRoleSet := make(map[string]rbacv1.ClusterRole)
8890
for i := range objs {
8991
switch t := objs[i].(type) {
9092
case *rbacv1.Role:
91-
roleSet[t.GetName()] = t
92-
}
93-
}
94-
95-
saToPermissions := make(map[string]operatorsv1alpha1.StrategyDeploymentPermissions)
96-
for _, dep := range c.Deployments {
97-
saName := dep.Spec.Template.Spec.ServiceAccountName
98-
if saName == "" {
99-
saName = defaultServiceAccountName
93+
roleSet[t.GetName()] = *t
94+
case *rbacv1.ClusterRole:
95+
cRoleSet[t.GetName()] = *t
10096
}
101-
saToPermissions[saName] = operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName}
10297
}
10398

104-
// Collect all role names by their corresponding service accounts via bindings. This lets us
99+
// Collect all role and cluster role names by their corresponding service accounts via bindings. This lets us
105100
// look up all service accounts a role is bound to and create one set of permissions per service account.
101+
saToPermissions := initPermissionSet(c.Deployments, extraSAs)
106102
for _, binding := range c.RoleBindings {
107-
if role, hasRole := roleSet[binding.RoleRef.Name]; hasRole {
108-
for _, subject := range binding.Subjects {
109-
if perm, hasSA := saToPermissions[subject.Name]; hasSA && subject.Kind == "ServiceAccount" {
110-
perm.Rules = append(perm.Rules, role.Rules...)
111-
saToPermissions[subject.Name] = perm
112-
}
103+
for _, subject := range binding.Subjects {
104+
perm, hasSA := saToPermissions[subject.Name]
105+
if subject.Kind != "ServiceAccount" || !hasSA {
106+
continue
107+
}
108+
var (
109+
rules []rbacv1.PolicyRule
110+
hasRole bool
111+
)
112+
switch binding.RoleRef.Kind {
113+
case "Role":
114+
role, has := roleSet[binding.RoleRef.Name]
115+
rules = role.Rules
116+
hasRole = has
117+
case "ClusterRole":
118+
role, has := cRoleSet[binding.RoleRef.Name]
119+
rules = role.Rules
120+
hasRole = has
121+
default:
122+
continue
123+
}
124+
if hasRole {
125+
perm.Rules = append(perm.Rules, rules...)
126+
saToPermissions[subject.Name] = perm
113127
}
114128
}
115129
}
@@ -121,39 +135,35 @@ func applyRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDeta
121135
perms = append(perms, perm)
122136
}
123137
}
138+
sort.Slice(perms, func(i, j int) bool {
139+
return perms[i].ServiceAccountName < perms[j].ServiceAccountName
140+
})
124141
strategy.Permissions = perms
125142
}
126143

127144
// applyClusterRoles applies ClusterRoles to strategy's clusterPermissions field by combining ClusterRoles
128145
// bound to ServiceAccounts into one set of clusterPermissions.
129-
func applyClusterRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) { //nolint:dupl
130-
objs, _ := c.SplitCSVClusterPermissionsObjects()
131-
roleSet := make(map[string]*rbacv1.ClusterRole)
146+
func applyClusterRoles(c *collector.Manifests, objs []client.Object, strategy *operatorsv1alpha1.StrategyDetailsDeployment, extraSAs []string) { //nolint:dupl
147+
roleSet := make(map[string]rbacv1.ClusterRole)
132148
for i := range objs {
133149
switch t := objs[i].(type) {
134150
case *rbacv1.ClusterRole:
135-
roleSet[t.GetName()] = t
151+
roleSet[t.GetName()] = *t
136152
}
137153
}
138154

139-
saToPermissions := make(map[string]operatorsv1alpha1.StrategyDeploymentPermissions)
140-
for _, dep := range c.Deployments {
141-
saName := dep.Spec.Template.Spec.ServiceAccountName
142-
if saName == "" {
143-
saName = defaultServiceAccountName
144-
}
145-
saToPermissions[saName] = operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName}
146-
}
147-
148155
// Collect all role names by their corresponding service accounts via bindings. This lets us
149156
// look up all service accounts a role is bound to and create one set of permissions per service account.
157+
saToPermissions := initPermissionSet(c.Deployments, extraSAs)
150158
for _, binding := range c.ClusterRoleBindings {
151-
if role, hasRole := roleSet[binding.RoleRef.Name]; hasRole {
152-
for _, subject := range binding.Subjects {
153-
if perm, hasSA := saToPermissions[subject.Name]; hasSA && subject.Kind == "ServiceAccount" {
154-
perm.Rules = append(perm.Rules, role.Rules...)
155-
saToPermissions[subject.Name] = perm
156-
}
159+
for _, subject := range binding.Subjects {
160+
perm, hasSA := saToPermissions[subject.Name]
161+
if !hasSA || subject.Kind != "ServiceAccount" {
162+
continue
163+
}
164+
if role, hasRole := roleSet[binding.RoleRef.Name]; hasRole {
165+
perm.Rules = append(perm.Rules, role.Rules...)
166+
saToPermissions[subject.Name] = perm
157167
}
158168
}
159169
}
@@ -165,9 +175,28 @@ func applyClusterRoles(c *collector.Manifests, strategy *operatorsv1alpha1.Strat
165175
perms = append(perms, perm)
166176
}
167177
}
178+
sort.Slice(perms, func(i, j int) bool {
179+
return perms[i].ServiceAccountName < perms[j].ServiceAccountName
180+
})
168181
strategy.ClusterPermissions = perms
169182
}
170183

184+
// initPermissionSet initializes a map of ServiceAccount name to permissions, which are empty.
185+
func initPermissionSet(deps []appsv1.Deployment, extraSAs []string) map[string]operatorsv1alpha1.StrategyDeploymentPermissions {
186+
saToPermissions := make(map[string]operatorsv1alpha1.StrategyDeploymentPermissions)
187+
for _, dep := range deps {
188+
saName := dep.Spec.Template.Spec.ServiceAccountName
189+
if saName == "" {
190+
saName = defaultServiceAccountName
191+
}
192+
saToPermissions[saName] = operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName}
193+
}
194+
for _, extraSA := range extraSAs {
195+
saToPermissions[extraSA] = operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: extraSA}
196+
}
197+
return saToPermissions
198+
}
199+
171200
// applyDeployments updates strategy's deployments with the Deployments
172201
// in the collector.
173202
func applyDeployments(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) {

0 commit comments

Comments
 (0)