tooling/mcerepkg/internal/rukpak/convert/registryv1.go (385 lines of code) (raw):

// Copyright 2025 Microsoft Corporation // // 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 convert import ( "bytes" "errors" "fmt" "io" "io/fs" "path/filepath" "strings" "testing/fstest" "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" apimachyaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" "github.com/operator-framework/api/pkg/operators/v1alpha1" registry "github.com/Azure/ARO-HCP/tooling/mcerepkg/internal/rukpak/operator-registry" "github.com/Azure/ARO-HCP/tooling/mcerepkg/internal/rukpak/util" ) type RegistryV1 struct { PackageName string CSV v1alpha1.ClusterServiceVersion CRDs []apiextensionsv1.CustomResourceDefinition Others []unstructured.Unstructured } type Plain struct { Objects []client.Object } func RegistryV1ToPlain(rv1 fs.FS, installNamespace string, watchNamespaces []string) (fs.FS, RegistryV1, error) { reg := RegistryV1{} fileData, err := fs.ReadFile(rv1, filepath.Join("metadata", "annotations.yaml")) if err != nil { return nil, reg, err } annotationsFile := registry.AnnotationsFile{} if err := yaml.Unmarshal(fileData, &annotationsFile); err != nil { return nil, reg, err } reg.PackageName = annotationsFile.Annotations.PackageName var objects []*unstructured.Unstructured const manifestsDir = "manifests" entries, err := fs.ReadDir(rv1, manifestsDir) if err != nil { return nil, reg, err } for _, e := range entries { if e.IsDir() { return nil, reg, fmt.Errorf("subdirectories are not allowed within the %q directory of the bundle image filesystem: found %q", manifestsDir, filepath.Join(manifestsDir, e.Name())) } fileData, err := fs.ReadFile(rv1, filepath.Join(manifestsDir, e.Name())) if err != nil { return nil, reg, err } dec := apimachyaml.NewYAMLOrJSONDecoder(bytes.NewReader(fileData), 1024) for { obj := unstructured.Unstructured{} err := dec.Decode(&obj) if errors.Is(err, io.EOF) { break } if err != nil { return nil, reg, fmt.Errorf("read %q: %v", e.Name(), err) } objects = append(objects, &obj) } } for _, obj := range objects { obj := obj switch obj.GetObjectKind().GroupVersionKind().Kind { case "ClusterServiceVersion": csv := v1alpha1.ClusterServiceVersion{} if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &csv); err != nil { return nil, reg, err } reg.CSV = csv case "CustomResourceDefinition": crd := apiextensionsv1.CustomResourceDefinition{} if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &crd); err != nil { return nil, reg, err } reg.CRDs = append(reg.CRDs, crd) default: reg.Others = append(reg.Others, *obj) } } plain, err := Convert(reg, installNamespace, watchNamespaces) if err != nil { return nil, reg, err } var manifest bytes.Buffer for _, obj := range plain.Objects { yamlData, err := yaml.Marshal(obj) if err != nil { return nil, reg, err } if _, err := fmt.Fprintf(&manifest, "---\n%s\n", string(yamlData)); err != nil { return nil, reg, err } } now := time.Now() plainFS := fstest.MapFS{ ".": &fstest.MapFile{ Data: nil, Mode: fs.ModeDir | 0o755, ModTime: now, }, "manifests": &fstest.MapFile{ Data: nil, Mode: fs.ModeDir | 0o755, ModTime: now, }, "manifests/manifest.yaml": &fstest.MapFile{ Data: manifest.Bytes(), Mode: 0o644, ModTime: now, }, } return plainFS, reg, nil } func validateTargetNamespaces(supportedInstallModes sets.Set[string], installNamespace string, targetNamespaces []string) error { set := sets.New[string](targetNamespaces...) switch { case set.Len() == 0 || (set.Len() == 1 && set.Has("")): if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeAllNamespaces)) { return nil } return fmt.Errorf("supported install modes %v do not support targeting all namespaces", sets.List(supportedInstallModes)) case set.Len() == 1 && !set.Has(""): if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeSingleNamespace)) { return nil } if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeOwnNamespace)) && targetNamespaces[0] == installNamespace { return nil } default: if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeMultiNamespace)) && !set.Has("") { return nil } } return fmt.Errorf("supported install modes %v do not support target namespaces %v", sets.List[string](supportedInstallModes), targetNamespaces) } func saNameOrDefault(saName string) string { if saName == "" { return "default" } return saName } func Convert(in RegistryV1, installNamespace string, targetNamespaces []string) (*Plain, error) { if installNamespace == "" { installNamespace = in.CSV.Annotations["operatorframework.io/suggested-namespace"] } if installNamespace == "" { installNamespace = fmt.Sprintf("%s-system", in.PackageName) } supportedInstallModes := sets.New[string]() for _, im := range in.CSV.Spec.InstallModes { if im.Supported { supportedInstallModes.Insert(string(im.Type)) } } if len(targetNamespaces) == 0 { if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeAllNamespaces)) { targetNamespaces = []string{""} } else if supportedInstallModes.Has(string(v1alpha1.InstallModeTypeOwnNamespace)) { targetNamespaces = []string{installNamespace} } } if err := validateTargetNamespaces(supportedInstallModes, installNamespace, targetNamespaces); err != nil { return nil, err } if len(in.CSV.Spec.APIServiceDefinitions.Owned) > 0 { return nil, fmt.Errorf("apiServiceDefintions are not supported") } if len(in.CSV.Spec.WebhookDefinitions) > 0 { return nil, fmt.Errorf("webhookDefinitions are not supported") } deployments := []appsv1.Deployment{} serviceAccounts := map[string]corev1.ServiceAccount{} for _, depSpec := range in.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs { annotations := util.MergeMaps(in.CSV.Annotations, depSpec.Spec.Template.Annotations) annotations["olm.targetNamespaces"] = strings.Join(targetNamespaces, ",") deployments = append(deployments, appsv1.Deployment{ TypeMeta: metav1.TypeMeta{ Kind: "Deployment", APIVersion: appsv1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Namespace: installNamespace, Name: depSpec.Name, Labels: depSpec.Label, Annotations: annotations, }, Spec: depSpec.Spec, }) saName := saNameOrDefault(depSpec.Spec.Template.Spec.ServiceAccountName) serviceAccounts[saName] = newServiceAccount(installNamespace, saName) } // NOTES: // 1. There's an extra Role for OperatorConditions: get/update/patch; resourceName=csv.name // - This is managed by the OperatorConditions controller here: https://github.com/operator-framework/operator-lifecycle-manager/blob/9ced412f3e263b8827680dc0ad3477327cd9a508/pkg/controller/operators/operatorcondition_controller.go#L106-L109 // 2. There's an extra RoleBinding for the above mentioned role. // - Every SA mentioned in the OperatorCondition.spec.serviceAccounts is a subject for this role binding: https://github.com/operator-framework/operator-lifecycle-manager/blob/9ced412f3e263b8827680dc0ad3477327cd9a508/pkg/controller/operators/operatorcondition_controller.go#L171-L177 // 3. strategySpec.permissions are _also_ given a clusterrole/clusterrole binding. // - (for AllNamespaces mode only?) // - (where does the extra namespaces get/list/watch rule come from?) roles := []rbacv1.Role{} roleBindings := []rbacv1.RoleBinding{} clusterRoles := []rbacv1.ClusterRole{} clusterRoleBindings := []rbacv1.ClusterRoleBinding{} permissions := in.CSV.Spec.InstallStrategy.StrategySpec.Permissions clusterPermissions := in.CSV.Spec.InstallStrategy.StrategySpec.ClusterPermissions allPermissions := append(permissions, clusterPermissions...) // Create all the service accounts for _, permission := range allPermissions { saName := saNameOrDefault(permission.ServiceAccountName) if _, ok := serviceAccounts[saName]; !ok { serviceAccounts[saName] = newServiceAccount(installNamespace, saName) } } // If we're in AllNamespaces mode, promote the permissions to clusterPermissions if len(targetNamespaces) == 1 && targetNamespaces[0] == "" { for _, p := range permissions { p.Rules = append(p.Rules, rbacv1.PolicyRule{ Verbs: []string{"get", "list", "watch"}, APIGroups: []string{corev1.GroupName}, Resources: []string{"namespaces"}, }) } clusterPermissions = append(clusterPermissions, permissions...) permissions = nil } for _, ns := range targetNamespaces { for _, permission := range permissions { saName := saNameOrDefault(permission.ServiceAccountName) name, err := generateName(fmt.Sprintf("%s-%s", in.CSV.Name, saName), permission) if err != nil { return nil, err } roles = append(roles, newRole(ns, name, permission.Rules)) roleBindings = append(roleBindings, newRoleBinding(ns, name, name, installNamespace, saName)) } } for _, permission := range clusterPermissions { saName := saNameOrDefault(permission.ServiceAccountName) name, err := generateName(fmt.Sprintf("%s-%s", in.CSV.Name, saName), permission) if err != nil { return nil, err } clusterRoles = append(clusterRoles, newClusterRole(name, permission.Rules)) clusterRoleBindings = append(clusterRoleBindings, newClusterRoleBinding(name, name, installNamespace, saName)) } objs := []client.Object{} for _, obj := range serviceAccounts { obj := obj if obj.GetName() != "default" { objs = append(objs, &obj) } } for _, obj := range roles { obj := obj objs = append(objs, &obj) } for _, obj := range roleBindings { obj := obj objs = append(objs, &obj) } for _, obj := range clusterRoles { obj := obj objs = append(objs, &obj) } for _, obj := range clusterRoleBindings { obj := obj objs = append(objs, &obj) } for _, obj := range in.CRDs { obj := obj objs = append(objs, &obj) } for _, obj := range in.Others { obj := obj obj.SetNamespace(installNamespace) objs = append(objs, &obj) } for _, obj := range deployments { obj := obj objs = append(objs, &obj) } return &Plain{Objects: objs}, nil } const maxNameLength = 63 func generateName(base string, o interface{}) (string, error) { hashStr, err := util.DeepHashObject(o) if err != nil { return "", err } if len(base)+len(hashStr) > maxNameLength { base = base[:maxNameLength-len(hashStr)-1] } return fmt.Sprintf("%s-%s", base, hashStr), nil } func newServiceAccount(namespace, name string) corev1.ServiceAccount { return corev1.ServiceAccount{ TypeMeta: metav1.TypeMeta{ Kind: "ServiceAccount", APIVersion: corev1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, }, } } func newRole(namespace, name string, rules []rbacv1.PolicyRule) rbacv1.Role { return rbacv1.Role{ TypeMeta: metav1.TypeMeta{ Kind: "Role", APIVersion: rbacv1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, }, Rules: rules, } } func newClusterRole(name string, rules []rbacv1.PolicyRule) rbacv1.ClusterRole { return rbacv1.ClusterRole{ TypeMeta: metav1.TypeMeta{ Kind: "ClusterRole", APIVersion: rbacv1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: name, }, Rules: rules, } } func newRoleBinding(namespace, name, roleName, saNamespace string, saNames ...string) rbacv1.RoleBinding { subjects := make([]rbacv1.Subject, 0, len(saNames)) for _, saName := range saNames { subjects = append(subjects, rbacv1.Subject{ Kind: "ServiceAccount", Namespace: saNamespace, Name: saName, }) } return rbacv1.RoleBinding{ TypeMeta: metav1.TypeMeta{ Kind: "RoleBinding", APIVersion: rbacv1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: name, }, Subjects: subjects, RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, Kind: "Role", Name: roleName, }, } } func newClusterRoleBinding(name, roleName, saNamespace string, saNames ...string) rbacv1.ClusterRoleBinding { subjects := make([]rbacv1.Subject, 0, len(saNames)) for _, saName := range saNames { subjects = append(subjects, rbacv1.Subject{ Kind: "ServiceAccount", Namespace: saNamespace, Name: saName, }) } return rbacv1.ClusterRoleBinding{ TypeMeta: metav1.TypeMeta{ Kind: "ClusterRoleBinding", APIVersion: rbacv1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ Name: name, }, Subjects: subjects, RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: roleName, }, } }