diff --git a/go.mod b/go.mod index b75c21bfee..a896f3f8a1 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.1 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 v28.0.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v27.5.1+incompatible // indirect diff --git a/go.sum b/go.sum index c70976dde4..0329d417d5 100644 --- a/go.sum +++ b/go.sum @@ -2228,8 +2228,6 @@ golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkg/controller/registry/resolver/rbac.go b/pkg/controller/registry/resolver/rbac.go index 4370e1b15f..679901c0e9 100644 --- a/pkg/controller/registry/resolver/rbac.go +++ b/pkg/controller/registry/resolver/rbac.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "hash/fnv" "math/big" "github.com/operator-framework/api/pkg/operators/v1alpha1" @@ -13,6 +14,7 @@ import ( corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilrand "k8s.io/apimachinery/pkg/util/rand" ) const maxNameLength = 63 @@ -29,6 +31,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 +245,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..2f93d2a140 100644 --- a/pkg/lib/kubernetes/pkg/util/hash/hash.go +++ b/pkg/lib/kubernetes/pkg/util/hash/hash.go @@ -17,39 +17,52 @@ limitations under the License. package hash import ( - "crypto/sha256" - "encoding/json" - "fmt" - "math/big" + "crypto/sha256" + "encoding/json" + "fmt" + "github.com/davecgh/go-spew/spew" + "hash" + "math/big" ) // DeepHashObject writes specified object to hash using the spew library // which follows pointers and prints actual values of the nested objects // ensuring the hash does not change when a pointer changes. func DeepHashObject(obj interface{}) (string, error) { - // While the most accurate encoding we could do for Kubernetes objects (runtime.Object) - // would use the API machinery serializers, those operate over entire objects - and - // we often need to operate on snippets. Checking with the experts and the implementation, - // we can see that the serializers are a thin wrapper over json.Marshal for encoding: - // https://github.com/kubernetes/kubernetes/blob/8509ab82b96caa2365552efa08c8ba8baf11c5ec/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go#L216-L247 - // Therefore, we can be confident that using json.Marshal() here will: - // 1. be stable & idempotent - the library sorts keys, etc. - // 2. be germane to our needs - only fields that serialize and are sent to the server - // will be encoded - - hasher := sha256.New224() - hasher.Reset() - encoder := json.NewEncoder(hasher) - if err := encoder.Encode(obj); err != nil { - return "", fmt.Errorf("couldn't encode object: %w", err) - } - - // base62(sha224(bytes)) is a useful hash and encoding for adding the contents of this - // to a Kubernetes identifier or other field which has length and character set requirements - var hash []byte - hash = hasher.Sum(hash) - - var i big.Int - i.SetBytes(hash[:]) - return i.Text(62), nil + // While the most accurate encoding we could do for Kubernetes objects (runtime.Object) + // would use the API machinery serializers, those operate over entire objects - and + // we often need to operate on snippets. Checking with the experts and the implementation, + // we can see that the serializers are a thin wrapper over json.Marshal for encoding: + // https://github.com/kubernetes/kubernetes/blob/8509ab82b96caa2365552efa08c8ba8baf11c5ec/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go#L216-L247 + // Therefore, we can be confident that using json.Marshal() here will: + // 1. be stable & idempotent - the library sorts keys, etc. + // 2. be germane to our needs - only fields that serialize and are sent to the server + // will be encoded + + hasher := sha256.New224() + hasher.Reset() + encoder := json.NewEncoder(hasher) + if err := encoder.Encode(obj); err != nil { + return "", fmt.Errorf("couldn't encode object: %w", err) + } + + // base62(sha224(bytes)) is a useful hash and encoding for adding the contents of this + // to a Kubernetes identifier or other field which has length and character set requirements + var hash []byte + hash = hasher.Sum(hash) + + var i big.Int + 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) }