Skip to content

🐛 Include rbac for legacy csv for csv service account #3532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
137 changes: 137 additions & 0 deletions pkg/controller/registry/resolver/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"hash/fnv"
"math/big"

"github.com/operator-framework/api/pkg/operators/v1alpha1"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions pkg/controller/registry/resolver/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
71 changes: 42 additions & 29 deletions pkg/lib/kubernetes/pkg/util/hash/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading