Skip to content
This repository has been archived by the owner on Dec 11, 2021. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
estroz committed Aug 10, 2020
1 parent 4dd9248 commit d01e9e7
Show file tree
Hide file tree
Showing 5 changed files with 720 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
admissionregv1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/version"

"github.com/operator-framework/operator-sdk/internal/generate/collector"
Expand All @@ -39,11 +40,10 @@ import (
func ApplyTo(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion) error {
// Apply manifests to the CSV object.
if err := apply(c, csv); err != nil {
return fmt.Errorf("error updating ClusterServiceVersion: %v", err)
return err
}

// Set fields required by namespaced operators. This is a no-op for cluster-
// scoped operators.
// Set fields required by namespaced operators. This is a no-op for cluster-scoped operators.
setNamespacedFields(csv)

// Sort all updated fields.
Expand All @@ -65,7 +65,7 @@ func apply(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion)

applyCustomResourceDefinitions(c, csv)
if err := applyCustomResources(c, csv); err != nil {
return fmt.Errorf("error applying Custom Resource: %v", err)
return fmt.Errorf("error applying Custom Resource examples to CSV %s: %v", csv.GetName(), err)
}
applyWebhooks(c, csv)
return nil
Expand All @@ -80,27 +80,93 @@ func getCSVInstallStrategy(csv *operatorsv1alpha1.ClusterServiceVersion) operato
return csv.Spec.InstallStrategy
}

// applyRoles updates strategy's permissions with the Roles in the collector.
func applyRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) {
// This service account exists in every namespace as the default.
const defaultServiceAccountName = "default"

// applyRoles applies Roles to strategy's permissions field by combining Roles bound to ServiceAccounts
// into one set of permissions.
func applyRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) { //nolint:dupl
objs, _ := c.SplitCSVPermissionsObjects()
roleSet := make(map[string]*rbacv1.Role)
for i := range objs {
switch t := objs[i].(type) {
case *rbacv1.Role:
roleSet[t.GetName()] = t
}
}

saToPermissions := make(map[string]operatorsv1alpha1.StrategyDeploymentPermissions)
for _, dep := range c.Deployments {
saName := dep.Spec.Template.Spec.ServiceAccountName
if saName == "" {
saName = defaultServiceAccountName
}
saToPermissions[saName] = operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName}
}

// Collect all role names by their corresponding service accounts via bindings. This lets us
// look up all service accounts a role is bound to and create one set of permissions per service account.
for _, binding := range c.RoleBindings {
if role, hasRole := roleSet[binding.RoleRef.Name]; hasRole {
for _, subject := range binding.Subjects {
if perm, hasSA := saToPermissions[subject.Name]; hasSA && subject.Kind == "ServiceAccount" {
perm.Rules = append(perm.Rules, role.Rules...)
saToPermissions[subject.Name] = perm
}
}
}
}

// Apply relevant roles to each service account.
perms := []operatorsv1alpha1.StrategyDeploymentPermissions{}
for _, role := range c.Roles {
perms = append(perms, operatorsv1alpha1.StrategyDeploymentPermissions{
ServiceAccountName: role.GetName(),
Rules: role.Rules,
})
for _, perm := range saToPermissions {
if len(perm.Rules) != 0 {
perms = append(perms, perm)
}
}
strategy.Permissions = perms
}

// applyClusterRoles updates strategy's cluserPermissions with the ClusterRoles
// in the collector.
func applyClusterRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) {
// applyClusterRoles applies ClusterRoles to strategy's clusterPermissions field by combining ClusterRoles
// bound to ServiceAccounts into one set of clusterPermissions.
func applyClusterRoles(c *collector.Manifests, strategy *operatorsv1alpha1.StrategyDetailsDeployment) { //nolint:dupl
objs, _ := c.SplitCSVClusterPermissionsObjects()
roleSet := make(map[string]*rbacv1.ClusterRole)
for i := range objs {
switch t := objs[i].(type) {
case *rbacv1.ClusterRole:
roleSet[t.GetName()] = t
}
}

saToPermissions := make(map[string]operatorsv1alpha1.StrategyDeploymentPermissions)
for _, dep := range c.Deployments {
saName := dep.Spec.Template.Spec.ServiceAccountName
if saName == "" {
saName = defaultServiceAccountName
}
saToPermissions[saName] = operatorsv1alpha1.StrategyDeploymentPermissions{ServiceAccountName: saName}
}

// Collect all role names by their corresponding service accounts via bindings. This lets us
// look up all service accounts a role is bound to and create one set of permissions per service account.
for _, binding := range c.ClusterRoleBindings {
if role, hasRole := roleSet[binding.RoleRef.Name]; hasRole {
for _, subject := range binding.Subjects {
if perm, hasSA := saToPermissions[subject.Name]; hasSA && subject.Kind == "ServiceAccount" {
perm.Rules = append(perm.Rules, role.Rules...)
saToPermissions[subject.Name] = perm
}
}
}
}

// Apply relevant roles to each service account.
perms := []operatorsv1alpha1.StrategyDeploymentPermissions{}
for _, role := range c.ClusterRoles {
perms = append(perms, operatorsv1alpha1.StrategyDeploymentPermissions{
ServiceAccountName: role.GetName(),
Rules: role.Rules,
})
for _, perm := range saToPermissions {
if len(perm.Rules) != 0 {
perms = append(perms, perm)
}
}
strategy.ClusterPermissions = perms
}
Expand Down
222 changes: 222 additions & 0 deletions internal/generate/collector/clusterserviceversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright 2020 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package collector

import (
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)

// TODO(estroz): there's a significant amount of code dupliation here, a byproduct of Go's type system.
// However at least a few bits can be refactored so each method is smaller.

const (
// This service account exists in every namespace as the default.
defaultServiceAccountName = "default"

serviceAccountKind = "ServiceAccount"
)

// SplitCSVPermissionsObjects splits roles that should be written to a CSV as permissions (in)
// from roles and role bindings that should be written directly to the bundle (out).
func (c *Manifests) SplitCSVPermissionsObjects() (in, out []runtime.Object) { //nolint:dupl
roleMap := make(map[string]*rbacv1.Role)
for i := range c.Roles {
roleMap[c.Roles[i].GetName()] = &c.Roles[i]
}
roleBindingMap := make(map[string]*rbacv1.RoleBinding)
for i := range c.RoleBindings {
roleBindingMap[c.RoleBindings[i].GetName()] = &c.RoleBindings[i]
}

// Check for unbound roles.
for roleName, role := range roleMap {
hasRef := false
for _, roleBinding := range roleBindingMap {
roleRef := roleBinding.RoleRef
if roleRef.Kind == "Role" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) {
if roleRef.Name == roleName {
hasRef = true
break
}
}
}
if !hasRef {
out = append(out, role)
delete(roleMap, roleName)
}
}

// If a role is bound and:
// 1. the binding only has one subject and it is a service account that maps to a deployment service account,
// add the role to in.
// 2. the binding only has one subject and it does not map to a deployment service account or is not a service account,
// add both role and binding to out.
// 3. the binding has more than one subject and:
// a. one of those subjects is a deployment's service account, add both role and binding to out and role to in.
// b. none of those subjects is a service account or maps to a deployment's service account, add both role and binding to out.
deploymentSANames := make(map[string]struct{})
for _, dep := range c.Deployments {
saName := dep.Spec.Template.Spec.ServiceAccountName
if saName == "" {
saName = defaultServiceAccountName
}
deploymentSANames[saName] = struct{}{}
}

inRoleNames := make(map[string]struct{})
outRoleNames := make(map[string]struct{})
outRoleBindingNames := make(map[string]struct{})
for _, binding := range c.RoleBindings {
roleRef := binding.RoleRef
if roleRef.Kind == "Role" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) {
numSubjects := len(binding.Subjects)
if numSubjects == 1 {
// cases (1) and (2).
if _, hasSA := deploymentSANames[binding.Subjects[0].Name]; hasSA && binding.Subjects[0].Kind == serviceAccountKind {
inRoleNames[roleRef.Name] = struct{}{}
} else {
outRoleNames[roleRef.Name] = struct{}{}
outRoleBindingNames[binding.GetName()] = struct{}{}
}
} else {
// case (3).
for _, subject := range binding.Subjects {
if _, hasSA := deploymentSANames[subject.Name]; hasSA && subject.Kind == serviceAccountKind {
// case (3a).
inRoleNames[roleRef.Name] = struct{}{}
}
}
// case (3b).
outRoleNames[roleRef.Name] = struct{}{}
outRoleBindingNames[binding.GetName()] = struct{}{}
}
}
}

for roleName := range inRoleNames {
if role, hasRoleName := roleMap[roleName]; hasRoleName {
in = append(in, role)
}
}
for roleName := range outRoleNames {
if role, hasRoleName := roleMap[roleName]; hasRoleName {
out = append(out, role)
}
}
for roleBindingName := range outRoleBindingNames {
if roleBinding, hasRoleBindingName := roleBindingMap[roleBindingName]; hasRoleBindingName {
out = append(out, roleBinding)
}
}

return in, out
}

// SplitCSVClusterPermissionsObjects splits cluster roles that should be written to a CSV as clusterPermissions (in)
// from cluster roles and cluster role bindings that should be written directly to the bundle (out).
func (c *Manifests) SplitCSVClusterPermissionsObjects() (in, out []runtime.Object) { //nolint:dupl
roleMap := make(map[string]*rbacv1.ClusterRole)
for i := range c.ClusterRoles {
roleMap[c.ClusterRoles[i].GetName()] = &c.ClusterRoles[i]
}
roleBindingMap := make(map[string]*rbacv1.ClusterRoleBinding)
for i := range c.ClusterRoleBindings {
roleBindingMap[c.ClusterRoleBindings[i].GetName()] = &c.ClusterRoleBindings[i]
}

// Check for unbound roles.
for roleName, role := range roleMap {
hasRef := false
for _, roleBinding := range roleBindingMap {
roleRef := roleBinding.RoleRef
if roleRef.Kind == "ClusterRole" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) {
if roleRef.Name == roleName {
hasRef = true
break
}
}
}
if !hasRef {
out = append(out, role)
delete(roleMap, roleName)
}
}

// If a role is bound and:
// 1. the binding only has one subject and it is a service account that maps to a deployment service account,
// add the role to in.
// 2. the binding only has one subject and it does not map to a deployment service account or is not a service account,
// add both role and binding to out.
// 3. the binding has more than one subject and:
// a. one of those subjects is a deployment's service account, add both role and binding to out and role to in.
// b. none of those subjects is a service account or maps to a deployment's service account, add both role and binding to out.
deploymentSANames := make(map[string]struct{})
for _, dep := range c.Deployments {
saName := dep.Spec.Template.Spec.ServiceAccountName
if saName == "" {
saName = defaultServiceAccountName
}
deploymentSANames[saName] = struct{}{}
}

inRoleNames := make(map[string]struct{})
outRoleNames := make(map[string]struct{})
outRoleBindingNames := make(map[string]struct{})
for _, binding := range c.ClusterRoleBindings {
roleRef := binding.RoleRef
if roleRef.Kind == "ClusterRole" && (roleRef.APIGroup == "" || roleRef.APIGroup == rbacv1.SchemeGroupVersion.Group) {
numSubjects := len(binding.Subjects)
if numSubjects == 1 {
// cases (1) and (2).
if _, hasSA := deploymentSANames[binding.Subjects[0].Name]; hasSA && binding.Subjects[0].Kind == serviceAccountKind {
inRoleNames[roleRef.Name] = struct{}{}
} else {
outRoleNames[roleRef.Name] = struct{}{}
outRoleBindingNames[binding.GetName()] = struct{}{}
}
} else {
// case (3).
for _, subject := range binding.Subjects {
if _, hasSA := deploymentSANames[subject.Name]; hasSA && subject.Kind == serviceAccountKind {
// case (3a).
inRoleNames[roleRef.Name] = struct{}{}
}
}
// case (3b).
outRoleNames[roleRef.Name] = struct{}{}
outRoleBindingNames[binding.GetName()] = struct{}{}
}
}
}

for roleName := range inRoleNames {
if role, hasRoleName := roleMap[roleName]; hasRoleName {
in = append(in, role)
}
}
for roleName := range outRoleNames {
if role, hasRoleName := roleMap[roleName]; hasRoleName {
out = append(out, role)
}
}
for roleBindingName := range outRoleBindingNames {
if roleBinding, hasRoleBindingName := roleBindingMap[roleBindingName]; hasRoleBindingName {
out = append(out, roleBinding)
}
}

return in, out
}

0 comments on commit d01e9e7

Please sign in to comment.