diff --git a/go.mod b/go.mod index 98a680d152..331acda5e6 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/containers/image/v5 v5.34.0 github.com/coreos/go-semver v0.3.1 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/distribution/reference v0.6.0 github.com/evanphx/json-patch v5.9.11+incompatible github.com/fsnotify/fsnotify v1.8.0 @@ -82,7 +83,6 @@ require ( github.com/containers/ocicrypt v1.2.1 // indirect github.com/containers/storage v1.57.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v27.5.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v27.5.1+incompatible // indirect diff --git a/pkg/controller/registry/resolver/rbac.go b/pkg/controller/registry/resolver/rbac.go index 4370e1b15f..ad307e5836 100644 --- a/pkg/controller/registry/resolver/rbac.go +++ b/pkg/controller/registry/resolver/rbac.go @@ -4,8 +4,11 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "hash/fnv" "math/big" + utilrand "k8s.io/apimachinery/pkg/util/rand" + "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" hashutil "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/hash" @@ -29,6 +32,16 @@ func generateName(base string, o interface{}) (string, error) { return fmt.Sprintf("%s-%s", base, hash), nil } +func legacyGenerateName(base string, o interface{}) string { + hasher := fnv.New32a() + hashutil.LegacyDeepHashObject(hasher, o) + hash := utilrand.SafeEncodeString(fmt.Sprint(hasher.Sum32())) + if len(base)+len(hash) > maxNameLength { + base = base[:maxNameLength-len(hash)-1] + } + return fmt.Sprintf("%s-%s", base, hash) +} + type OperatorPermissions struct { ServiceAccount *corev1.ServiceAccount Roles []*rbacv1.Role @@ -233,3 +246,128 @@ func RBACForClusterServiceVersion(csv *v1alpha1.ClusterServiceVersion) (map[stri } return permissions, nil } + +func LegacyRBACForClusterServiceVersion(csv *v1alpha1.ClusterServiceVersion) (map[string]*OperatorPermissions, error) { + permissions := map[string]*OperatorPermissions{} + + // Use a StrategyResolver to get the strategy details + strategyResolver := install.StrategyResolver{} + strategy, err := strategyResolver.UnmarshalStrategy(csv.Spec.InstallStrategy) + if err != nil { + return nil, err + } + + // Assume the strategy is for a deployment + strategyDetailsDeployment, ok := strategy.(*v1alpha1.StrategyDetailsDeployment) + if !ok { + return nil, fmt.Errorf("could not assert strategy implementation as deployment for CSV %s", csv.GetName()) + } + + // Resolve Permissions + for _, permission := range strategyDetailsDeployment.Permissions { + // Create ServiceAccount if necessary + if _, ok := permissions[permission.ServiceAccountName]; !ok { + serviceAccount := &corev1.ServiceAccount{} + serviceAccount.SetNamespace(csv.GetNamespace()) + serviceAccount.SetName(permission.ServiceAccountName) + ownerutil.AddNonBlockingOwner(serviceAccount, csv) + + permissions[permission.ServiceAccountName] = NewOperatorPermissions(serviceAccount) + } + + // Create Role + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: legacyGenerateName(fmt.Sprintf("%s-%s", csv.GetName(), permission.ServiceAccountName), []interface{}{csv.GetName(), permission}), + Namespace: csv.GetNamespace(), + OwnerReferences: []metav1.OwnerReference{ownerutil.NonBlockingOwner(csv)}, + Labels: ownerutil.OwnerLabel(csv, v1alpha1.ClusterServiceVersionKind), + }, + Rules: permission.Rules, + } + hash, err := PolicyRuleHashLabelValue(permission.Rules) + if err != nil { + return nil, fmt.Errorf("failed to hash permission rules: %w", err) + } + role.Labels[ContentHashLabelKey] = hash + permissions[permission.ServiceAccountName].AddRole(role) + + // Create RoleBinding + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: role.GetName(), + Namespace: csv.GetNamespace(), + OwnerReferences: []metav1.OwnerReference{ownerutil.NonBlockingOwner(csv)}, + Labels: ownerutil.OwnerLabel(csv, v1alpha1.ClusterServiceVersionKind), + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: role.GetName(), + APIGroup: rbacv1.GroupName}, + Subjects: []rbacv1.Subject{{ + Kind: "ServiceAccount", + Name: permission.ServiceAccountName, + Namespace: csv.GetNamespace(), + }}, + } + hash, err = RoleReferenceAndSubjectHashLabelValue(roleBinding.RoleRef, roleBinding.Subjects) + if err != nil { + return nil, fmt.Errorf("failed to hash binding content: %w", err) + } + roleBinding.Labels[ContentHashLabelKey] = hash + permissions[permission.ServiceAccountName].AddRoleBinding(roleBinding) + } + + // Resolve ClusterPermissions as StepResources + for _, permission := range strategyDetailsDeployment.ClusterPermissions { + // Create ServiceAccount if necessary + if _, ok := permissions[permission.ServiceAccountName]; !ok { + serviceAccount := &corev1.ServiceAccount{} + ownerutil.AddOwner(serviceAccount, csv, false, false) + serviceAccount.SetName(permission.ServiceAccountName) + + permissions[permission.ServiceAccountName] = NewOperatorPermissions(serviceAccount) + } + + // Create ClusterRole + role := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: legacyGenerateName(csv.GetName(), []interface{}{csv.GetName(), csv.GetNamespace(), permission}), + Labels: ownerutil.OwnerLabel(csv, v1alpha1.ClusterServiceVersionKind), + }, + Rules: permission.Rules, + } + hash, err := PolicyRuleHashLabelValue(permission.Rules) + if err != nil { + return nil, fmt.Errorf("failed to hash permission rules: %w", err) + } + role.Labels[ContentHashLabelKey] = hash + permissions[permission.ServiceAccountName].AddClusterRole(role) + + // Create ClusterRoleBinding + roleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: role.GetName(), + Namespace: csv.GetNamespace(), + Labels: ownerutil.OwnerLabel(csv, v1alpha1.ClusterServiceVersionKind), + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: role.GetName(), + APIGroup: rbacv1.GroupName, + }, + Subjects: []rbacv1.Subject{{ + Kind: "ServiceAccount", + Name: permission.ServiceAccountName, + Namespace: csv.GetNamespace(), + }}, + } + hash, err = RoleReferenceAndSubjectHashLabelValue(roleBinding.RoleRef, roleBinding.Subjects) + if err != nil { + return nil, fmt.Errorf("failed to hash binding content: %w", err) + } + roleBinding.Labels[ContentHashLabelKey] = hash + permissions[permission.ServiceAccountName].AddClusterRoleBinding(roleBinding) + } + return permissions, nil +} diff --git a/pkg/controller/registry/resolver/steps.go b/pkg/controller/registry/resolver/steps.go index dbe3be8534..f2e8740bdf 100644 --- a/pkg/controller/registry/resolver/steps.go +++ b/pkg/controller/registry/resolver/steps.go @@ -200,6 +200,13 @@ func NewServiceAccountStepResources(csv *v1alpha1.ClusterServiceVersion, catalog if err != nil { return nil, err } + legacyPerms, err := LegacyRBACForClusterServiceVersion(csv) + if err != nil { + return nil, err + } + for k, v := range legacyPerms { + operatorPermissions[k] = v + } for _, perms := range operatorPermissions { if perms.ServiceAccount.Name != "default" { diff --git a/pkg/lib/kubernetes/pkg/util/hash/hash.go b/pkg/lib/kubernetes/pkg/util/hash/hash.go index 993f15274a..35e1d563cb 100644 --- a/pkg/lib/kubernetes/pkg/util/hash/hash.go +++ b/pkg/lib/kubernetes/pkg/util/hash/hash.go @@ -20,7 +20,10 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "hash" "math/big" + + "github.com/davecgh/go-spew/spew" ) // DeepHashObject writes specified object to hash using the spew library @@ -53,3 +56,14 @@ func DeepHashObject(obj interface{}) (string, error) { i.SetBytes(hash[:]) return i.Text(62), nil } + +func LegacyDeepHashObject(hasher hash.Hash, objectToWrite interface{}) { + hasher.Reset() + printer := spew.ConfigState{ + Indent: " ", + SortKeys: true, + DisableMethods: true, + SpewKeys: true, + } + printer.Fprintf(hasher, "%#v", objectToWrite) +}