From 54073deebffb0479d59448f0c86d9133495ab488 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Mon, 31 Mar 2025 13:50:17 +0200 Subject: [PATCH 1/3] Add CRDs field to RegistryV1 struct Signed-off-by: Per Goncalves da Silva --- .../operator-controller/rukpak/convert/registryv1.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/operator-controller/rukpak/convert/registryv1.go b/internal/operator-controller/rukpak/convert/registryv1.go index 155b86be8..289ac6648 100644 --- a/internal/operator-controller/rukpak/convert/registryv1.go +++ b/internal/operator-controller/rukpak/convert/registryv1.go @@ -13,6 +13,7 @@ import ( 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" @@ -32,6 +33,7 @@ import ( type RegistryV1 struct { PackageName string CSV v1alpha1.ClusterServiceVersion + CRDs []apiextensionsv1.CustomResourceDefinition Others []unstructured.Unstructured } @@ -121,6 +123,12 @@ func ParseFS(rv1 fs.FS) (RegistryV1, error) { } reg.CSV = csv foundCSV = true + case "CustomResourceDefinition": + crd := apiextensionsv1.CustomResourceDefinition{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(info.Object.(*unstructured.Unstructured).Object, &crd); err != nil { + return err + } + reg.CRDs = append(reg.CRDs, crd) default: reg.Others = append(reg.Others, *info.Object.(*unstructured.Unstructured)) } @@ -362,6 +370,9 @@ func Convert(in RegistryV1, installNamespace string, targetNamespaces []string) obj := obj objs = append(objs, &obj) } + for _, obj := range in.CRDs { + objs = append(objs, &obj) + } for _, obj := range in.Others { obj := obj supported, namespaced := registrybundle.IsSupported(obj.GetKind()) From 32e5198a2306d6de254cb3adf4dc05bcee5c8388 Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Wed, 2 Apr 2025 18:30:47 +0200 Subject: [PATCH 2/3] add render package Signed-off-by: Per Goncalves da Silva --- .../rukpak/convert/registryv1.go | 2 +- .../rukpak/convert/render/generate.go | 191 +++ .../rukpak/convert/render/generate_test.go | 1132 +++++++++++++++++ .../rukpak/convert/render/mutate.go | 77 ++ .../rukpak/convert/render/render.go | 75 ++ .../rukpak/convert/render/render_test.go | 165 +++ .../rukpak/convert/render/resources.go | 160 +++ .../rukpak/convert/render/resources_test.go | 210 +++ .../rukpak/convert/render/webhooks.go | 267 ++++ 9 files changed, 2278 insertions(+), 1 deletion(-) create mode 100644 internal/operator-controller/rukpak/convert/render/generate.go create mode 100644 internal/operator-controller/rukpak/convert/render/generate_test.go create mode 100644 internal/operator-controller/rukpak/convert/render/mutate.go create mode 100644 internal/operator-controller/rukpak/convert/render/render.go create mode 100644 internal/operator-controller/rukpak/convert/render/render_test.go create mode 100644 internal/operator-controller/rukpak/convert/render/resources.go create mode 100644 internal/operator-controller/rukpak/convert/render/resources_test.go create mode 100644 internal/operator-controller/rukpak/convert/render/webhooks.go diff --git a/internal/operator-controller/rukpak/convert/registryv1.go b/internal/operator-controller/rukpak/convert/registryv1.go index 289ac6648..6df085927 100644 --- a/internal/operator-controller/rukpak/convert/registryv1.go +++ b/internal/operator-controller/rukpak/convert/registryv1.go @@ -371,7 +371,7 @@ func Convert(in RegistryV1, installNamespace string, targetNamespaces []string) objs = append(objs, &obj) } for _, obj := range in.CRDs { - objs = append(objs, &obj) + objs = append(objs, obj.DeepCopy()) } for _, obj := range in.Others { obj := obj diff --git a/internal/operator-controller/rukpak/convert/render/generate.go b/internal/operator-controller/rukpak/convert/render/generate.go new file mode 100644 index 000000000..c3ebd9ffc --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/generate.go @@ -0,0 +1,191 @@ +package render + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" +) + +type UniqueNameGenerator func(string, interface{}) (string, error) + +type Options struct { + InstallNamespace string + TargetNamespaces []string + UniqueNameGenerator UniqueNameGenerator +} + +type ResourceGenerator func(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) + +func (g ResourceGenerator) GenerateResources(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { + return g(rv1, opts) +} + +func ChainedResourceGenerator(resourceGenerators ...ResourceGenerator) ResourceGenerator { + return func(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { + //nolint:prealloc + var renderedObjects []client.Object + for _, generator := range resourceGenerators { + objs, err := generator(rv1, opts) + if err != nil { + return nil, err + } + renderedObjects = append(renderedObjects, objs...) + } + return renderedObjects, nil + } +} + +func BundleDeploymentGenerator(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { + //nolint:prealloc + var objs []client.Object + for _, depSpec := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs { + annotations := util.MergeMaps(rv1.CSV.Annotations, depSpec.Spec.Template.Annotations) + annotations["olm.targetNamespaces"] = strings.Join(opts.TargetNamespaces, ",") + depSpec.Spec.Template.Annotations = annotations + + // Hardcode the deployment with RevisionHistoryLimit=1 (something OLMv0 does, not sure why) + depSpec.Spec.RevisionHistoryLimit = ptr.To(int32(1)) + + objs = append(objs, + GenerateDeploymentResource( + depSpec.Name, + opts.InstallNamespace, + WithDeploymentSpec(depSpec.Spec), + WithLabels(depSpec.Label), + ), + ) + } + return objs, nil +} + +func BundlePermissionsGenerator(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { + permissions := rv1.CSV.Spec.InstallStrategy.StrategySpec.Permissions + + // If we're in AllNamespaces mode permissions will be treated as clusterPermissions + if len(opts.TargetNamespaces) == 1 && opts.TargetNamespaces[0] == "" { + return nil, nil + } + + var objs []client.Object + for _, ns := range opts.TargetNamespaces { + for _, permission := range permissions { + saName := saNameOrDefault(permission.ServiceAccountName) + name, err := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission) + if err != nil { + return nil, err + } + + objs = append(objs, + GenerateRoleResource(name, ns, WithRules(permission.Rules...)), + GenerateRoleBindingResource( + name, + ns, + WithSubjects(rbacv1.Subject{Kind: "ServiceAccount", Namespace: opts.InstallNamespace, Name: saName}), + WithRoleRef(rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "Role", Name: name}), + ), + ) + } + } + return objs, nil +} + +func BundleClusterPermissionsGenerator(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { + clusterPermissions := rv1.CSV.Spec.InstallStrategy.StrategySpec.ClusterPermissions + + // If we're in AllNamespaces mode, promote the permissions to clusterPermissions + if len(opts.TargetNamespaces) == 1 && opts.TargetNamespaces[0] == "" { + for _, p := range rv1.CSV.Spec.InstallStrategy.StrategySpec.Permissions { + p.Rules = append(p.Rules, rbacv1.PolicyRule{ + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{corev1.GroupName}, + Resources: []string{"namespaces"}, + }) + clusterPermissions = append(clusterPermissions, p) + } + } + + //nolint:prealloc + var objs []client.Object + for _, permission := range clusterPermissions { + saName := saNameOrDefault(permission.ServiceAccountName) + name, err := opts.UniqueNameGenerator(fmt.Sprintf("%s-%s", rv1.CSV.Name, saName), permission) + if err != nil { + return nil, err + } + objs = append(objs, + GenerateClusterRoleResource(name, WithRules(permission.Rules...)), + GenerateClusterRoleBindingResource( + name, + WithSubjects(rbacv1.Subject{Kind: "ServiceAccount", Namespace: opts.InstallNamespace, Name: saName}), + WithRoleRef(rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: name}), + ), + ) + } + return objs, nil +} + +func BundleServiceAccountGenerator(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { + allPermissions := append( + rv1.CSV.Spec.InstallStrategy.StrategySpec.Permissions, + rv1.CSV.Spec.InstallStrategy.StrategySpec.ClusterPermissions..., + ) + + var objs []client.Object + serviceAccountNames := sets.Set[string]{} + for _, permission := range allPermissions { + serviceAccountNames.Insert(saNameOrDefault(permission.ServiceAccountName)) + } + + for _, serviceAccountName := range serviceAccountNames.UnsortedList() { + // no need to generate the default service account + if serviceAccountName != "default" { + objs = append(objs, GenerateServiceAccountResource(serviceAccountName, opts.InstallNamespace)) + } + } + return objs, nil +} + +func BundleCRDGenerator(rv1 *convert.RegistryV1, _ Options) ([]client.Object, error) { + //nolint:prealloc + var objs []client.Object + for _, crd := range rv1.CRDs { + objs = append(objs, crd.DeepCopy()) + } + return objs, nil +} + +func BundleResourceGenerator(rv1 *convert.RegistryV1, _ Options) ([]client.Object, error) { + //nolint:prealloc + var objs []client.Object + for _, res := range rv1.Others { + supported, namespaced := registrybundle.IsSupported(res.GetKind()) + if !supported { + return nil, fmt.Errorf("bundle contains unsupported resource: Name: %v, Kind: %v", res.GetName(), res.GetKind()) + } + + obj := res.DeepCopy() + if namespaced { + obj.SetNamespace(res.GetNamespace()) + } + + objs = append(objs, obj) + } + return objs, nil +} + +func saNameOrDefault(saName string) string { + if saName == "" { + return "default" + } + return saName +} diff --git a/internal/operator-controller/rukpak/convert/render/generate_test.go b/internal/operator-controller/rukpak/convert/render/generate_test.go new file mode 100644 index 000000000..e6d1643aa --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/generate_test.go @@ -0,0 +1,1132 @@ +package render_test + +import ( + "cmp" + "slices" + "testing" + + "github.com/stretchr/testify/require" + 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/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert/render" +) + +func Test_BundleDeploymentGenerator(t *testing.T) { + for _, tc := range []struct { + name string + bundle *convert.RegistryV1 + opts render.Options + expectedResources []client.Object + }{ + { + name: "generates deployment resources", + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithAnnotations(map[string]string{ + "csv": "annotation", + }), + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "deployment-one", + Label: map[string]string{ + "bar": "foo", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "pod": "annotation", + }, + }, + }, + }, + }, + v1alpha1.StrategyDeploymentSpec{ + Name: "deployment-two", + Spec: appsv1.DeploymentSpec{}, + }, + ), + ), + }, + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{"watch-namespace-one", "watch-namespace-two"}, + }, + expectedResources: []client.Object{ + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: appsv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "install-namespace", + Name: "deployment-one", + Labels: map[string]string{ + "bar": "foo", + }, + }, + Spec: appsv1.DeploymentSpec{ + RevisionHistoryLimit: ptr.To(int32(1)), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "csv": "annotation", + "olm.targetNamespaces": "watch-namespace-one,watch-namespace-two", + "pod": "annotation", + }, + }, + }, + }, + }, + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: appsv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "install-namespace", + Name: "deployment-two", + }, + Spec: appsv1.DeploymentSpec{ + RevisionHistoryLimit: ptr.To(int32(1)), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "csv": "annotation", + "olm.targetNamespaces": "watch-namespace-one,watch-namespace-two", + }, + }, + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + objs, err := render.BundleDeploymentGenerator(tc.bundle, tc.opts) + require.NoError(t, err) + require.Equal(t, tc.expectedResources, objs) + }) + } +} + +func Test_BundlePermissionsGenerator(t *testing.T) { + fakeUniqueNameGenerator := func(base string, _ interface{}) (string, error) { + return base, nil + } + + for _, tc := range []struct { + name string + opts render.Options + bundle *convert.RegistryV1 + expectedResources []client.Object + }{ + { + name: "does not generate any resources when in AllNamespaces mode (target namespace is [''])", + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{""}, + UniqueNameGenerator: fakeUniqueNameGenerator, + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-one", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + ), + ), + }, + expectedResources: nil, + }, + { + name: "generates role and rolebinding for permission service-account when in Single/OwnNamespace mode (target namespace contains a single namespace)", + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{"watch-namespace"}, + UniqueNameGenerator: fakeUniqueNameGenerator, + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-one", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + ), + ), + }, + expectedResources: []client.Object{ + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-one", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-one", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-one", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "csv-service-account-one", + }, + }, + }, + }, + { + name: "generates role and rolebinding for permission service-account for each target namespace when in MultiNamespace install mode (target namespace contains multiple namespaces)", + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{"watch-namespace", "watch-namespace-two"}, + UniqueNameGenerator: fakeUniqueNameGenerator, + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-one", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + ), + ), + }, + expectedResources: []client.Object{ + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-one", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-one", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-one", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "csv-service-account-one", + }, + }, + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace-two", + Name: "csv-service-account-one", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace-two", + Name: "csv-service-account-one", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-one", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "csv-service-account-one", + }, + }, + }, + }, + { + name: "generates role and rolebinding for each permission service-account", + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{"watch-namespace"}, + UniqueNameGenerator: fakeUniqueNameGenerator, + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-one", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-two", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + ), + ), + }, + expectedResources: []client.Object{ + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-one", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-one", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-one", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "csv-service-account-one", + }, + }, + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-two", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-two", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-two", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "csv-service-account-two", + }, + }, + }, + }, + { + name: "treats empty service account as 'default' service account", + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{"watch-namespace"}, + UniqueNameGenerator: fakeUniqueNameGenerator, + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + ), + ), + }, + expectedResources: []client.Object{ + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-default", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-default", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "default", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "csv-default", + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + objs, err := render.BundlePermissionsGenerator(tc.bundle, tc.opts) + require.NoError(t, err) + for i := range objs { + require.Equal(t, tc.expectedResources[i], objs[i], "failed to find expected resource at index %d", i) + } + require.Equal(t, len(tc.expectedResources), len(objs)) + }) + } +} + +func Test_BundleClusterPermissionsGenerator(t *testing.T) { + fakeUniqueNameGenerator := func(base string, _ interface{}) (string, error) { + return base, nil + } + + for _, tc := range []struct { + name string + opts render.Options + bundle *convert.RegistryV1 + expectedResources []client.Object + }{ + { + name: "promotes permissions to clusters permissions and adds namespace policy rule when in AllNamespaces mode (target namespace is [''])", + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{""}, + UniqueNameGenerator: fakeUniqueNameGenerator, + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-one", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-two", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + ), + ), + }, + expectedResources: []client.Object{ + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-one", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{corev1.GroupName}, + Resources: []string{"namespaces"}, + }, + }, + }, + &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-one", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-one", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "csv-service-account-one", + }, + }, + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-two", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{corev1.GroupName}, + Resources: []string{"namespaces"}, + }, + }, + }, + &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-two", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-two", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "csv-service-account-two", + }, + }, + }, + }, + { + name: "generates clusterroles and clusterrolebindings for clusterpermissions", + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{"watch-namespace"}, + UniqueNameGenerator: fakeUniqueNameGenerator, + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithClusterPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-one", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-two", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + ), + ), + }, + expectedResources: []client.Object{ + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-one", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-one", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-one", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "csv-service-account-one", + }, + }, + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-two", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-two", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "service-account-two", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "csv-service-account-two", + }, + }, + }, + }, + { + name: "treats empty service accounts as 'default' service account", + opts: render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{"watch-namespace"}, + UniqueNameGenerator: fakeUniqueNameGenerator, + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithClusterPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + ), + ), + }, + expectedResources: []client.Object{ + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-default", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-default", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "default", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "csv-default", + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + objs, err := render.BundleClusterPermissionsGenerator(tc.bundle, tc.opts) + require.NoError(t, err) + for i := range objs { + require.Equal(t, tc.expectedResources[i], objs[i], "failed to find expected resource at index %d", i) + } + require.Equal(t, len(tc.expectedResources), len(objs)) + }) + } +} + +func Test_BundleServiceAccountGenerator(t *testing.T) { + for _, tc := range []struct { + name string + opts render.Options + bundle *convert.RegistryV1 + expectedResources []client.Object + }{ + { + name: "generates unique set of clusterpermissions and permissions service accounts in the install namespace", + opts: render.Options{ + InstallNamespace: "install-namespace", + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-1", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-2", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + ), + WithClusterPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-2", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account-3", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments"}, + Verbs: []string{"create"}, + }, + }, + }, + ), + ), + }, + expectedResources: []client.Object{ + &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "service-account-1", + Namespace: "install-namespace", + }, + }, + &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "service-account-2", + Namespace: "install-namespace", + }, + }, + &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "service-account-3", + Namespace: "install-namespace", + }, + }, + }, + }, + { + name: "treats empty service accounts as default and doesn't generate them", + opts: render.Options{ + InstallNamespace: "install-namespace", + }, + bundle: &convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + ), + WithClusterPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + ), + ), + }, + expectedResources: nil, + }, + } { + t.Run(tc.name, func(t *testing.T) { + objs, err := render.BundleServiceAccountGenerator(tc.bundle, tc.opts) + require.NoError(t, err) + slices.SortFunc(objs, func(a, b client.Object) int { + return cmp.Compare(a.GetName(), b.GetName()) + }) + for i := range objs { + require.Equal(t, tc.expectedResources[i], objs[i], "failed to find expected resource at index %d", i) + } + require.Equal(t, len(tc.expectedResources), len(objs)) + }) + } +} + +func Test_BundleCRDGenerator_Succeeds(t *testing.T) { + opts := render.Options{ + InstallNamespace: "install-namespace", + TargetNamespaces: []string{""}, + } + + bundle := &convert.RegistryV1{ + CRDs: []apiextensionsv1.CustomResourceDefinition{ + {ObjectMeta: metav1.ObjectMeta{Name: "crd-one"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "crd-two"}}, + }, + } + + objs, err := render.BundleCRDGenerator(bundle, opts) + require.NoError(t, err) + require.Equal(t, []client.Object{ + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "crd-one"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "crd-two"}}, + }, objs) +} + +func Test_BundleResourceGenerator_Succeeds(t *testing.T) { + opts := render.Options{ + InstallNamespace: "install-namespace", + } + + bundle := &convert.RegistryV1{ + Others: []unstructured.Unstructured{ + toUnstructured( + &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bundled-service", + }, + }, + ), + toUnstructured( + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bundled-clusterrole", + }, + }, + ), + }, + } + + objs, err := render.BundleResourceGenerator(bundle, opts) + require.NoError(t, err) + require.Len(t, objs, 2) +} + +type CSVOption func(version *v1alpha1.ClusterServiceVersion) + +func WithName(name string) CSVOption { + return func(csv *v1alpha1.ClusterServiceVersion) { + csv.Name = name + } +} + +func WithStrategyDeploymentSpecs(strategyDeploymentSpecs ...v1alpha1.StrategyDeploymentSpec) CSVOption { + return func(csv *v1alpha1.ClusterServiceVersion) { + csv.Spec.InstallStrategy.StrategySpec.DeploymentSpecs = strategyDeploymentSpecs + } +} + +func WithOwnedCRDs(crdDesc ...v1alpha1.CRDDescription) CSVOption { + return func(csv *v1alpha1.ClusterServiceVersion) { + csv.Spec.CustomResourceDefinitions.Owned = crdDesc + } +} + +func WithAnnotations(annotations map[string]string) CSVOption { + return func(csv *v1alpha1.ClusterServiceVersion) { + csv.Annotations = annotations + } +} + +func WithPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) CSVOption { + return func(csv *v1alpha1.ClusterServiceVersion) { + csv.Spec.InstallStrategy.StrategySpec.Permissions = permissions + } +} + +func WithClusterPermissions(permissions ...v1alpha1.StrategyDeploymentPermissions) CSVOption { + return func(csv *v1alpha1.ClusterServiceVersion) { + csv.Spec.InstallStrategy.StrategySpec.ClusterPermissions = permissions + } +} + +func toUnstructured(obj client.Object) unstructured.Unstructured { + gvk := obj.GetObjectKind().GroupVersionKind() + + var u unstructured.Unstructured + uObj, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + unstructured.RemoveNestedField(uObj, "metadata", "creationTimestamp") + unstructured.RemoveNestedField(uObj, "status") + u.Object = uObj + u.SetGroupVersionKind(gvk) + return u +} + +func MakeCSV(opts ...CSVOption) v1alpha1.ClusterServiceVersion { + csv := v1alpha1.ClusterServiceVersion{} + for _, opt := range opts { + opt(&csv) + } + return csv +} diff --git a/internal/operator-controller/rukpak/convert/render/mutate.go b/internal/operator-controller/rukpak/convert/render/mutate.go new file mode 100644 index 000000000..775442d79 --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/mutate.go @@ -0,0 +1,77 @@ +package render + +import ( + "iter" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ResourceMutator func(client.Object) error + +func (r ResourceMutator) Mutate(obj client.Object) error { + return r(obj) +} + +func (r ResourceMutator) MutateObjects(objs iter.Seq[client.Object]) error { + for obj := range objs { + if err := r.Mutate(obj); err != nil { + return err + } + } + return nil +} + +type ResourceMutators []ResourceMutator + +func (g *ResourceMutators) Append(generator ...ResourceMutator) { + *g = append(*g, generator...) +} + +func (g *ResourceMutators) Mutate(obj client.Object) error { + for _, mutator := range *g { + if err := mutator.Mutate(obj); err != nil { + return err + } + } + return nil +} + +func (g *ResourceMutators) MutateObjects(objs iter.Seq[client.Object]) error { + for obj := range objs { + if err := g.Mutate(obj); err != nil { + return err + } + } + return nil +} + +type ResourceMutatorFactory func() (ResourceMutators, error) + +func (m ResourceMutatorFactory) MakeResourceMutators() (ResourceMutators, error) { + return m() +} + +type ChainedResourceMutatorFactory []ResourceMutatorFactory + +func (c ChainedResourceMutatorFactory) MakeResourceMutators() (ResourceMutators, error) { + var resourceMutators []ResourceMutator + for _, mutatorFactory := range c { + mutators, err := mutatorFactory.MakeResourceMutators() + if err != nil { + return nil, err + } + resourceMutators = append(resourceMutators, mutators...) + } + return resourceMutators, nil +} + +func CustomResourceDefinitionMutator(name string, mutator func(crd *apiextensionsv1.CustomResourceDefinition) error) ResourceMutator { + return func(obj client.Object) error { + crd, ok := obj.(*apiextensionsv1.CustomResourceDefinition) + if obj.GetName() != name || !ok { + return nil + } + return mutator(crd) + } +} diff --git a/internal/operator-controller/rukpak/convert/render/render.go b/internal/operator-controller/rukpak/convert/render/render.go new file mode 100644 index 000000000..6c999bc8e --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/render.go @@ -0,0 +1,75 @@ +package render + +import ( + "fmt" + "slices" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" +) + +const maxNameLength = 63 + +type Option func(*options) + +type options struct { + UniqueNameGenerator UniqueNameGenerator +} + +func (o *options) apply(opts ...Option) *options { + for _, opt := range opts { + opt(o) + } + return o +} + +type BundleRenderer struct { + ResourceGenerators []ResourceGenerator + ResourceMutatorFactories []ResourceMutatorFactory +} + +func (r BundleRenderer) Render(rv1 convert.RegistryV1, installNamespace string, watchNamespaces []string, opts ...Option) ([]client.Object, error) { + renderOptions := (&options{ + UniqueNameGenerator: DefaultUniqueNameGenerator, + }).apply(opts...) + + // create generation options + genOpts := Options{ + InstallNamespace: installNamespace, + TargetNamespaces: watchNamespaces, + UniqueNameGenerator: renderOptions.UniqueNameGenerator, + } + + // generate object mutators + objMutators, err := ChainedResourceMutatorFactory(r.ResourceMutatorFactories).MakeResourceMutators() + if err != nil { + return nil, err + } + + // generate bundle objects + objs, err := ChainedResourceGenerator(r.ResourceGenerators...).GenerateResources(&rv1, genOpts) + if err != nil { + return nil, err + } + + // mutate objects + if err := objMutators.MutateObjects(slices.Values(objs)); err != nil { + return nil, err + } + + return objs, nil +} + +func DefaultUniqueNameGenerator(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 +} diff --git a/internal/operator-controller/rukpak/convert/render/render_test.go b/internal/operator-controller/rukpak/convert/render/render_test.go new file mode 100644 index 000000000..606d0e8b2 --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/render_test.go @@ -0,0 +1,165 @@ +package render_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert/render" +) + +func Test_BundleRenderer_NoConfig(t *testing.T) { + renderer := render.BundleRenderer{} + objs, err := renderer.Render(convert.RegistryV1{}, "", nil) + require.NoError(t, err) + require.Empty(t, objs) +} + +func Test_BundleRenderer_CallsResourceGenerators(t *testing.T) { + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *convert.RegistryV1, opts render.Options) ([]client.Object, error) { + return []client.Object{&corev1.Namespace{}, &corev1.Service{}}, nil + }, + func(rv1 *convert.RegistryV1, opts render.Options) ([]client.Object, error) { + return []client.Object{&appsv1.Deployment{}}, nil + }, + }, + } + objs, err := renderer.Render(convert.RegistryV1{}, "", nil) + require.NoError(t, err) + require.Equal(t, []client.Object{&corev1.Namespace{}, &corev1.Service{}, &appsv1.Deployment{}}, objs) +} + +func Test_BundleRenderer_CallsResourceMutators(t *testing.T) { + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *convert.RegistryV1, opts render.Options) ([]client.Object, error) { + return []client.Object{&corev1.Namespace{}, &corev1.Service{}}, nil + }, + }, + ResourceMutatorFactories: []render.ResourceMutatorFactory{ + func() (render.ResourceMutators, error) { + return []render.ResourceMutator{ + func(object client.Object) error { + switch object.(type) { + case *corev1.Namespace: + object.SetName("some-namespace") + case *corev1.Service: + object.SetName("some-service") + } + return nil + }, + func(object client.Object) error { + object.SetLabels(map[string]string{ + "some": "label", + }) + return nil + }, + }, nil + }, + func() (render.ResourceMutators, error) { + return []render.ResourceMutator{ + func(object client.Object) error { + object.SetAnnotations(map[string]string{ + "some": "annotation", + }) + return nil + }, + }, nil + }, + }, + } + objs, err := renderer.Render(convert.RegistryV1{}, "", nil) + require.NoError(t, err) + require.Equal(t, []client.Object{ + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-namespace", + Labels: map[string]string{ + "some": "label", + }, + Annotations: map[string]string{ + "some": "annotation", + }, + }, + }, + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-service", + Labels: map[string]string{ + "some": "label", + }, + Annotations: map[string]string{ + "some": "annotation", + }, + }, + }, + }, objs, objs) +} + +func Test_BundleRenderer_ReturnsResourceGeneratorErrors(t *testing.T) { + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *convert.RegistryV1, opts render.Options) ([]client.Object, error) { + return []client.Object{&corev1.Namespace{}, &corev1.Service{}}, nil + }, + func(rv1 *convert.RegistryV1, opts render.Options) ([]client.Object, error) { + return nil, fmt.Errorf("generator error") + }, + }, + } + objs, err := renderer.Render(convert.RegistryV1{}, "", nil) + require.Nil(t, objs) + require.Error(t, err) + require.Contains(t, err.Error(), "generator error") +} + +func Test_BundleRenderer_ReturnsResourceMutatorFactoryErrors(t *testing.T) { + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *convert.RegistryV1, opts render.Options) ([]client.Object, error) { + return []client.Object{&corev1.Namespace{}, &corev1.Service{}}, nil + }, + }, + ResourceMutatorFactories: []render.ResourceMutatorFactory{ + func() (render.ResourceMutators, error) { + return nil, errors.New("mutator factory error") + }, + }, + } + objs, err := renderer.Render(convert.RegistryV1{}, "", nil) + require.Nil(t, objs) + require.Error(t, err) + require.Contains(t, err.Error(), "mutator factory error") +} + +func Test_BundleRenderer_ReturnsResourceMutatorErrors(t *testing.T) { + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *convert.RegistryV1, opts render.Options) ([]client.Object, error) { + return []client.Object{&corev1.Namespace{}, &corev1.Service{}}, nil + }, + }, + ResourceMutatorFactories: []render.ResourceMutatorFactory{ + func() (render.ResourceMutators, error) { + return []render.ResourceMutator{ + func(object client.Object) error { + return errors.New("mutator error") + }, + }, nil + }, + }, + } + objs, err := renderer.Render(convert.RegistryV1{}, "", nil) + require.Nil(t, objs) + require.Error(t, err) + require.Contains(t, err.Error(), "mutator error") +} diff --git a/internal/operator-controller/rukpak/convert/render/resources.go b/internal/operator-controller/rukpak/convert/render/resources.go new file mode 100644 index 000000000..4664b02ac --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/resources.go @@ -0,0 +1,160 @@ +package render + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ResourceGenerationOption = func(client.Object) +type ResourceGenerationOptions []ResourceGenerationOption + +func (r ResourceGenerationOptions) ApplyTo(obj client.Object) client.Object { + if obj == nil { + return nil + } + for _, opt := range r { + if opt != nil { + opt(obj) + } + } + return obj +} + +func WithSubjects(subjects ...rbacv1.Subject) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *rbacv1.RoleBinding: + o.Subjects = subjects + case *rbacv1.ClusterRoleBinding: + o.Subjects = subjects + } + } +} + +func WithRoleRef(roleRef rbacv1.RoleRef) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *rbacv1.RoleBinding: + o.RoleRef = roleRef + case *rbacv1.ClusterRoleBinding: + o.RoleRef = roleRef + } + } +} + +func WithRules(rules ...rbacv1.PolicyRule) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *rbacv1.Role: + o.Rules = rules + case *rbacv1.ClusterRole: + o.Rules = rules + } + } +} + +func WithDeploymentSpec(depSpec appsv1.DeploymentSpec) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *appsv1.Deployment: + o.Spec = depSpec + } + } +} + +func WithLabels(labels map[string]string) func(client.Object) { + return func(obj client.Object) { + obj.SetLabels(labels) + } +} + +func GenerateServiceAccountResource(name string, namespace string, opts ...ResourceGenerationOption) *corev1.ServiceAccount { + return ResourceGenerationOptions(opts).ApplyTo( + &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }, + ).(*corev1.ServiceAccount) +} + +func GenerateRoleResource(name string, namespace string, opts ...ResourceGenerationOption) *rbacv1.Role { + return ResourceGenerationOptions(opts).ApplyTo( + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }, + ).(*rbacv1.Role) +} + +func GenerateClusterRoleResource(name string, opts ...ResourceGenerationOption) *rbacv1.ClusterRole { + return ResourceGenerationOptions(opts).ApplyTo( + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, + ).(*rbacv1.ClusterRole) +} + +func GenerateClusterRoleBindingResource(name string, opts ...ResourceGenerationOption) *rbacv1.ClusterRoleBinding { + return ResourceGenerationOptions(opts).ApplyTo( + &rbacv1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, + ).(*rbacv1.ClusterRoleBinding) +} + +func GenerateRoleBindingResource(name string, namespace string, opts ...ResourceGenerationOption) *rbacv1.RoleBinding { + return ResourceGenerationOptions(opts).ApplyTo( + &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }, + ).(*rbacv1.RoleBinding) +} + +func GenerateDeploymentResource(name string, namespace string, opts ...ResourceGenerationOption) *appsv1.Deployment { + return ResourceGenerationOptions(opts).ApplyTo( + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: appsv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }, + ).(*appsv1.Deployment) +} diff --git a/internal/operator-controller/rukpak/convert/render/resources_test.go b/internal/operator-controller/rukpak/convert/render/resources_test.go new file mode 100644 index 000000000..1b491cee6 --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/resources_test.go @@ -0,0 +1,210 @@ +package render_test + +import ( + "maps" + "slices" + "strings" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert/render" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" +) + +func Test_OptionsApplyToExecutesIgnoresNil(t *testing.T) { + opts := []render.ResourceGenerationOption{ + func(object client.Object) { + object.SetAnnotations(util.MergeMaps(object.GetAnnotations(), map[string]string{"h": ""})) + }, + nil, + func(object client.Object) { + object.SetAnnotations(util.MergeMaps(object.GetAnnotations(), map[string]string{"i": ""})) + }, + nil, + } + + require.Nil(t, render.ResourceGenerationOptions(nil).ApplyTo(nil)) + require.Nil(t, render.ResourceGenerationOptions([]render.ResourceGenerationOption{}).ApplyTo(nil)) + + obj := render.ResourceGenerationOptions(opts).ApplyTo(&corev1.ConfigMap{}) + require.Equal(t, "hi", strings.Join(slices.Sorted(maps.Keys(obj.GetAnnotations())), "")) +} + +func Test_GenerateServiceAccount(t *testing.T) { + svc := render.GenerateServiceAccountResource("my-sa", "my-namespace") + require.NotNil(t, svc) + require.Equal(t, "my-sa", svc.Name) + require.Equal(t, "my-namespace", svc.Namespace) +} + +func Test_GenerateRole(t *testing.T) { + role := render.GenerateRoleResource("my-role", "my-namespace") + require.NotNil(t, role) + require.Equal(t, "my-role", role.Name) + require.Equal(t, "my-namespace", role.Namespace) +} + +func Test_GenerateRoleBinding(t *testing.T) { + roleBinding := render.GenerateRoleBindingResource("my-role-binding", "my-namespace") + require.NotNil(t, roleBinding) + require.Equal(t, "my-role-binding", roleBinding.Name) + require.Equal(t, "my-namespace", roleBinding.Namespace) +} + +func Test_GenerateClusterRole(t *testing.T) { + clusterRole := render.GenerateClusterRoleResource("my-cluster-role") + require.NotNil(t, clusterRole) + require.Equal(t, "my-cluster-role", clusterRole.Name) +} + +func Test_GenerateClusterRoleBinding(t *testing.T) { + clusterRoleBinding := render.GenerateClusterRoleBindingResource("my-cluster-role-binding") + require.NotNil(t, clusterRoleBinding) + require.Equal(t, "my-cluster-role-binding", clusterRoleBinding.Name) +} + +func Test_GenerateDeployment(t *testing.T) { + deployment := render.GenerateDeploymentResource("my-deployment", "my-namespace") + require.NotNil(t, deployment) + require.Equal(t, "my-deployment", deployment.Name) + require.Equal(t, "my-namespace", deployment.Namespace) +} + +func Test_WithSubjects(t *testing.T) { + for _, tc := range []struct { + name string + subjects []rbacv1.Subject + }{ + { + name: "empty", + subjects: []rbacv1.Subject{}, + }, { + name: "nil", + subjects: nil, + }, { + name: "single subject", + subjects: []rbacv1.Subject{ + { + APIGroup: rbacv1.GroupName, + Kind: rbacv1.ServiceAccountKind, + Name: "my-sa", + Namespace: "my-namespace", + }, + }, + }, { + name: "multiple subjects", + subjects: []rbacv1.Subject{ + { + APIGroup: rbacv1.GroupName, + Kind: rbacv1.ServiceAccountKind, + Name: "my-sa", + Namespace: "my-namespace", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + roleBinding := render.GenerateRoleBindingResource("my-role", "my-namespace", render.WithSubjects(tc.subjects...)) + require.NotNil(t, roleBinding) + require.Equal(t, roleBinding.Subjects, tc.subjects) + + clusterRoleBinding := render.GenerateClusterRoleBindingResource("my-role", render.WithSubjects(tc.subjects...)) + require.NotNil(t, clusterRoleBinding) + require.Equal(t, clusterRoleBinding.Subjects, tc.subjects) + }) + } +} + +func Test_WithRules(t *testing.T) { + for _, tc := range []struct { + name string + rules []rbacv1.PolicyRule + }{ + { + name: "empty", + rules: []rbacv1.PolicyRule{}, + }, { + name: "nil", + rules: nil, + }, { + name: "single subject", + rules: []rbacv1.PolicyRule{ + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + }, + }, { + name: "multiple subjects", + rules: []rbacv1.PolicyRule{ + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + ResourceNames: []string{"my-resource"}, + }, { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"appsv1"}, + Resources: []string{"deployments", "replicasets", "statefulsets"}, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + role := render.GenerateRoleResource("my-role", "my-namespace", render.WithRules(tc.rules...)) + require.NotNil(t, role) + require.Equal(t, role.Rules, tc.rules) + + clusterRole := render.GenerateClusterRoleResource("my-role", render.WithRules(tc.rules...)) + require.NotNil(t, clusterRole) + require.Equal(t, clusterRole.Rules, tc.rules) + }) + } +} + +func Test_WithRoleRef(t *testing.T) { + roleRef := rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: "my-role", + } + + roleBinding := render.GenerateRoleBindingResource("my-role-binding", "my-namespace", render.WithRoleRef(roleRef)) + require.NotNil(t, roleBinding) + require.Equal(t, roleRef, roleBinding.RoleRef) + + clusterRoleBinding := render.GenerateClusterRoleBindingResource("my-cluster-role-binding", render.WithRoleRef(roleRef)) + require.NotNil(t, clusterRoleBinding) + require.Equal(t, roleRef, clusterRoleBinding.RoleRef) +} + +func Test_WithLabels(t *testing.T) { + for _, tc := range []struct { + name string + labels map[string]string + }{ + { + name: "empty", + labels: map[string]string{}, + }, { + name: "nil", + labels: nil, + }, { + name: "not empty", + labels: map[string]string{ + "foo": "bar", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + dep := render.GenerateDeploymentResource("my-deployment", "my-namespace", render.WithLabels(tc.labels)) + require.NotNil(t, dep) + require.Equal(t, tc.labels, dep.Labels) + }) + } +} diff --git a/internal/operator-controller/rukpak/convert/render/webhooks.go b/internal/operator-controller/rukpak/convert/render/webhooks.go new file mode 100644 index 000000000..017111b03 --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/webhooks.go @@ -0,0 +1,267 @@ +package render + +import ( + "cmp" + "fmt" + "slices" + "strconv" + "strings" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" +) + +func WithServiceSpec(serviceSpec corev1.ServiceSpec) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *corev1.Service: + o.Spec = serviceSpec + } + } +} + +func WithValidatingWebhooks(webhooks ...admissionregistrationv1.ValidatingWebhook) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *admissionregistrationv1.ValidatingWebhookConfiguration: + o.Webhooks = webhooks + } + } +} + +func WithMutatingWebhooks(webhooks ...admissionregistrationv1.MutatingWebhook) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *admissionregistrationv1.MutatingWebhookConfiguration: + o.Webhooks = webhooks + } + } +} + +func BundleWebhookResourceGenerator(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { + //nolint:prealloc + var objs []client.Object + webhookServicePortsByDeployment := map[string]sets.Set[corev1.ServicePort]{} + for _, wh := range rv1.CSV.Spec.WebhookDefinitions { + // collect webhook service ports + if _, ok := webhookServicePortsByDeployment[wh.DeploymentName]; !ok { + webhookServicePortsByDeployment[wh.DeploymentName] = sets.Set[corev1.ServicePort]{} + } + webhookServicePortsByDeployment[wh.DeploymentName].Insert(getWebhookServicePort(wh)) + + // collect webhook configurations and crd conversions + switch wh.Type { + case v1alpha1.ValidatingAdmissionWebhook: + objs = append(objs, + GenerateValidatingWebhookConfigurationResource( + wh.GenerateName, + WithValidatingWebhooks( + admissionregistrationv1.ValidatingWebhook{ + Name: wh.GenerateName, + Rules: wh.Rules, + FailurePolicy: wh.FailurePolicy, + MatchPolicy: wh.MatchPolicy, + ObjectSelector: wh.ObjectSelector, + SideEffects: wh.SideEffects, + TimeoutSeconds: wh.TimeoutSeconds, + AdmissionReviewVersions: wh.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Namespace: opts.InstallNamespace, + Name: getWebhookServiceName(wh.DeploymentName), + Path: wh.WebhookPath, + Port: &wh.ContainerPort, + }, + }, + }, + ), + ), + ) + case v1alpha1.MutatingAdmissionWebhook: + objs = append(objs, + GenerateMutatingWebhookConfigurationResource( + wh.GenerateName, + WithMutatingWebhooks( + admissionregistrationv1.MutatingWebhook{ + Name: wh.GenerateName, + Rules: wh.Rules, + FailurePolicy: wh.FailurePolicy, + MatchPolicy: wh.MatchPolicy, + ObjectSelector: wh.ObjectSelector, + SideEffects: wh.SideEffects, + TimeoutSeconds: wh.TimeoutSeconds, + AdmissionReviewVersions: wh.AdmissionReviewVersions, + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Namespace: opts.InstallNamespace, + Name: getWebhookServiceName(wh.DeploymentName), + Path: wh.WebhookPath, + Port: &wh.ContainerPort, + }, + }, + ReinvocationPolicy: wh.ReinvocationPolicy, + }, + ), + ), + ) + case v1alpha1.ConversionWebhook: + // dealt with using resource mutators + } + } + + for _, deploymentSpec := range rv1.CSV.Spec.InstallStrategy.StrategySpec.DeploymentSpecs { + if _, ok := webhookServicePortsByDeployment[deploymentSpec.Name]; !ok { + continue + } + + servicePorts := webhookServicePortsByDeployment[deploymentSpec.Name] + ports := servicePorts.UnsortedList() + slices.SortFunc(ports, func(a, b corev1.ServicePort) int { + return cmp.Compare(a.Name, b.Name) + }) + + var labelSelector map[string]string + if deploymentSpec.Spec.Selector != nil { + labelSelector = deploymentSpec.Spec.Selector.MatchLabels + } + + objs = append(objs, + GenerateServiceResource( + getWebhookServiceName(deploymentSpec.Name), + opts.InstallNamespace, + WithServiceSpec( + corev1.ServiceSpec{ + Ports: ports, + Selector: labelSelector, + }, + ), + ), + ) + } + + return objs, nil +} + +func BundleConversionWebhookResourceMutator(rv1 *convert.RegistryV1, opts Options) (ResourceMutators, error) { + mutators := ResourceMutators{} + + // generate mutators based on conversion webhook definitions on the CRD + for _, wh := range rv1.CSV.Spec.WebhookDefinitions { + switch wh.Type { + case v1alpha1.ConversionWebhook: + conversionWebhookPath := "/" + if wh.WebhookPath != nil { + conversionWebhookPath = *wh.WebhookPath + } + conversion := &apiextensionsv1.CustomResourceConversion{ + Strategy: apiextensionsv1.WebhookConverter, + Webhook: &apiextensionsv1.WebhookConversion{ + ClientConfig: &apiextensionsv1.WebhookClientConfig{ + Service: &apiextensionsv1.ServiceReference{ + Namespace: opts.InstallNamespace, + Name: getWebhookServiceName(wh.DeploymentName), + Path: &conversionWebhookPath, + Port: &wh.ContainerPort, + }, + }, + ConversionReviewVersions: wh.AdmissionReviewVersions, + }, + } + + for _, conversionCRD := range wh.ConversionCRDs { + mutators.Append( + CustomResourceDefinitionMutator(conversionCRD, func(crd *apiextensionsv1.CustomResourceDefinition) error { + crd.Spec.Conversion = conversion + return nil + }), + ) + } + } + } + + // generate mutators based on conversion webhook configurations already present on the CRDs + for _, crd := range rv1.CRDs { + if crd.Spec.Conversion != nil && crd.Spec.Conversion.Webhook != nil && crd.Spec.Conversion.Webhook.ClientConfig != nil && crd.Spec.Conversion.Webhook.ClientConfig.Service != nil { + mutators.Append( + CustomResourceDefinitionMutator(crd.GetName(), func(crd *apiextensionsv1.CustomResourceDefinition) error { + crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace = opts.InstallNamespace + return nil + }), + ) + } + } + + return mutators, nil +} + +func GenerateValidatingWebhookConfigurationResource(name string, opts ...ResourceGenerationOption) *admissionregistrationv1.ValidatingWebhookConfiguration { + return ResourceGenerationOptions(opts).ApplyTo( + &admissionregistrationv1.ValidatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: "ValidatingWebhookConfiguration", + APIVersion: admissionregistrationv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, + ).(*admissionregistrationv1.ValidatingWebhookConfiguration) +} + +func GenerateMutatingWebhookConfigurationResource(name string, opts ...ResourceGenerationOption) *admissionregistrationv1.MutatingWebhookConfiguration { + return ResourceGenerationOptions(opts).ApplyTo( + &admissionregistrationv1.MutatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: "MutatingWebhookConfiguration", + APIVersion: admissionregistrationv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, + ).(*admissionregistrationv1.MutatingWebhookConfiguration) +} + +func GenerateServiceResource(name string, namespace string, opts ...ResourceGenerationOption) *corev1.Service { + return ResourceGenerationOptions(opts).ApplyTo(&corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }).(*corev1.Service) +} + +func getWebhookServicePort(wh v1alpha1.WebhookDescription) corev1.ServicePort { + containerPort := int32(443) + if wh.ContainerPort > 0 { + containerPort = wh.ContainerPort + } + + targetPort := intstr.FromInt32(containerPort) + if wh.TargetPort != nil { + targetPort = *wh.TargetPort + } + + return corev1.ServicePort{ + Name: strconv.Itoa(int(containerPort)), + Port: containerPort, + TargetPort: targetPort, + } +} + +func getWebhookServiceName(deploymentName string) string { + return fmt.Sprintf("%s-service", strings.ReplaceAll(deploymentName, ".", "-")) +} From 40841f8a69b386f90f8d83d71188424e77412dac Mon Sep 17 00:00:00 2001 From: Per Goncalves da Silva Date: Thu, 3 Apr 2025 15:28:47 +0200 Subject: [PATCH 3/3] Add webhook render generators, converters, and resources Signed-off-by: Per Goncalves da Silva --- go.mod | 20 +- go.sum | 60 +++--- .../rukpak/convert/render/certmanager.go | 124 +++++++++++ .../rukpak/convert/render/certprovider.go | 24 +++ .../rukpak/convert/render/generate.go | 1 + .../rukpak/convert/render/mutate.go | 55 ++++- .../rukpak/convert/render/render.go | 23 ++- .../rukpak/convert/render/render_test.go | 8 +- .../rukpak/convert/render/resources.go | 69 +++++++ .../rukpak/convert/render/webhooks.go | 193 ++++++++++++------ 10 files changed, 465 insertions(+), 112 deletions(-) create mode 100644 internal/operator-controller/rukpak/convert/render/certmanager.go create mode 100644 internal/operator-controller/rukpak/convert/render/certprovider.go diff --git a/go.mod b/go.mod index f04717cd8..eef2b0cb0 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/BurntSushi/toml v1.5.0 github.com/Masterminds/semver/v3 v3.3.1 github.com/blang/semver/v4 v4.0.0 + github.com/cert-manager/cert-manager v1.17.1 github.com/containerd/containerd v1.7.27 github.com/containers/image/v5 v5.34.3 github.com/fsnotify/fsnotify v1.8.0 @@ -43,7 +44,7 @@ require ( ) require ( - cel.dev/expr v0.18.0 // indirect + cel.dev/expr v0.19.1 // indirect dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect @@ -55,7 +56,7 @@ require ( github.com/Microsoft/hcsshim v0.12.9 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -90,7 +91,7 @@ require ( github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-errors/errors v1.4.2 // indirect @@ -121,10 +122,10 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -207,12 +208,12 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect go.opentelemetry.io/otel/metric v1.33.0 // indirect go.opentelemetry.io/otel/sdk v1.33.0 // indirect go.opentelemetry.io/otel/trace v1.33.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/proto/otlp v1.4.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.37.0 // indirect @@ -234,7 +235,8 @@ require ( k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect k8s.io/kubectl v0.32.2 // indirect oras.land/oras-go v1.2.5 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1 // indirect + sigs.k8s.io/gateway-api v1.1.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/kustomize/api v0.18.0 // indirect sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect diff --git a/go.sum b/go.sum index 51e8817b5..62837eccf 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= -cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= @@ -34,8 +34,8 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpH github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -51,6 +51,8 @@ github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cert-manager/cert-manager v1.17.1 h1:Aig+lWMoLsmpGd9TOlTvO4t0Ah3D+/vGB37x/f+ZKt0= +github.com/cert-manager/cert-manager v1.17.1/go.mod h1:zeG4D+AdzqA7hFMNpYCJgcQ2VOfFNBa+Jzm3kAwiDU4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= @@ -140,8 +142,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -259,16 +261,16 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 h1:JYghRBlGCZyCF2wNUJ8W0cwaQdtpcssJ4CgC406g+WU= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc= @@ -339,8 +341,8 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= -github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -523,12 +525,12 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= -go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28= -go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q= -go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E= -go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE= -go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50= +go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= +go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4= +go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw= +go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w= +go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY= +go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= @@ -539,8 +541,8 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.54.0 h1:WWL67oxtknNVMb70lJXxXr go.opentelemetry.io/contrib/bridges/prometheus v0.54.0/go.mod h1:LqNcnXmyULp8ertk4hUTVtSUvKXj4h1Mx7gUCSSr/q0= go.opentelemetry.io/contrib/exporters/autoexport v0.54.0 h1:dTmcmVm4J54IRPGm5oVjLci1uYat4UDea84E2tyBaAk= go.opentelemetry.io/contrib/exporters/autoexport v0.54.0/go.mod h1:zPp5Fwpq2Hc7xMtVttg6GhZMcfTESjVbY9ONw2o/Dc4= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= @@ -551,10 +553,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6f go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 h1:xvhQxJ/C9+RTnAj5DpTg7LSM1vbbMTiXt7e9hsfqHNw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0/go.mod h1:Fcvs2Bz1jkDM+Wf5/ozBGmi3tQ/c9zPKLnsipnfhGAo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= go.opentelemetry.io/otel/exporters/prometheus v0.51.0 h1:G7uexXb/K3T+T9fNLCCKncweEtNEBMTO+46hKX5EdKw= @@ -577,8 +579,8 @@ go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4Jjx go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -791,10 +793,12 @@ k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJ k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1 h1:uOuSLOMBWkJH0TWa9X6l+mj5nZdm6Ay6Bli8HL8rNfk= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.1/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.20.2 h1:/439OZVxoEc02psi1h4QO3bHzTgu49bb347Xp4gW1pc= sigs.k8s.io/controller-runtime v0.20.2/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= +sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= diff --git a/internal/operator-controller/rukpak/convert/render/certmanager.go b/internal/operator-controller/rukpak/convert/render/certmanager.go new file mode 100644 index 000000000..2700b8b3d --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/certmanager.go @@ -0,0 +1,124 @@ +package render + +import ( + "errors" + "fmt" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + certmanagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/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" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util" +) + +const ( + certManagerInjectCAAnnotation = "cert-manager.io/inject-ca-from" +) + +type CertManagerProvider struct{} + +func (p CertManagerProvider) InjectCABundle(obj client.Object, cfg CertificateProvisioningConfig) error { + switch obj.(type) { + case *admissionregistrationv1.ValidatingWebhookConfiguration: + p.addCAInjectionAnnotation(obj, cfg.Namespace, cfg.CertName) + case *admissionregistrationv1.MutatingWebhookConfiguration: + p.addCAInjectionAnnotation(obj, cfg.Namespace, cfg.CertName) + case *apiextensionsv1.CustomResourceDefinition: + p.addCAInjectionAnnotation(obj, cfg.Namespace, cfg.CertName) + } + return nil +} + +func (p CertManagerProvider) GetCertSecretInfo(cfg CertificateProvisioningConfig) CertSecretInfo { + return CertSecretInfo{ + SecretName: cfg.CertName, + PrivateKeyKey: "crt.key", + CertificateKey: "crt.crt", + } +} + +func (p CertManagerProvider) AdditionalObjects(cfg CertificateProvisioningConfig) ([]unstructured.Unstructured, error) { + var ( + objs []unstructured.Unstructured + errs []error + ) + + issuer := &certmanagerv1.Issuer{ + TypeMeta: metav1.TypeMeta{ + APIVersion: certmanagerv1.SchemeGroupVersion.String(), + Kind: "Issuer", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-selfsigned-issuer", cfg.CertName), + Namespace: cfg.Namespace, + }, + Spec: certmanagerv1.IssuerSpec{ + IssuerConfig: certmanagerv1.IssuerConfig{ + SelfSigned: &certmanagerv1.SelfSignedIssuer{}, + }, + }, + } + issuerObj, err := toUnstructured(issuer) + if err != nil { + errs = append(errs, err) + } else { + objs = append(objs, *issuerObj) + } + + certificate := &certmanagerv1.Certificate{ + TypeMeta: metav1.TypeMeta{ + APIVersion: certmanagerv1.SchemeGroupVersion.String(), + Kind: "Certificate", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.CertName, + Namespace: cfg.Namespace, + }, + Spec: certmanagerv1.CertificateSpec{ + SecretName: cfg.CertName, + Usages: []certmanagerv1.KeyUsage{certmanagerv1.UsageServerAuth}, + DNSNames: []string{fmt.Sprintf("%s.%s.svc", cfg.WebhookServiceName, cfg.Namespace)}, + IssuerRef: certmanagermetav1.ObjectReference{ + Name: issuer.GetName(), + }, + }, + } + certObj, err := toUnstructured(certificate) + if err != nil { + errs = append(errs, err) + } else { + objs = append(objs, *certObj) + } + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + return objs, nil +} + +func (p CertManagerProvider) addCAInjectionAnnotation(obj client.Object, certNamespace string, certName string) { + injectionAnnotation := map[string]string{ + certManagerInjectCAAnnotation: fmt.Sprintf("%s/%s", certNamespace, certName), + } + obj.SetAnnotations(util.MergeMaps(obj.GetAnnotations(), injectionAnnotation)) +} + +func toUnstructured(obj client.Object) (*unstructured.Unstructured, error) { + gvk := obj.GetObjectKind().GroupVersionKind() + + var u unstructured.Unstructured + uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, fmt.Errorf("convert %s %q to unstructured: %w", gvk.Kind, obj.GetName(), err) + } + unstructured.RemoveNestedField(uObj, "metadata", "creationTimestamp") + unstructured.RemoveNestedField(uObj, "status") + u.Object = uObj + u.SetGroupVersionKind(gvk) + return &u, nil +} diff --git a/internal/operator-controller/rukpak/convert/render/certprovider.go b/internal/operator-controller/rukpak/convert/render/certprovider.go new file mode 100644 index 000000000..dedc78a81 --- /dev/null +++ b/internal/operator-controller/rukpak/convert/render/certprovider.go @@ -0,0 +1,24 @@ +package render + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type CertificateProvisioningConfig struct { + WebhookServiceName string + CertName string + Namespace string +} + +type CertificateProvider interface { + InjectCABundle(obj client.Object, cfg CertificateProvisioningConfig) error + AdditionalObjects(cfg CertificateProvisioningConfig) ([]unstructured.Unstructured, error) + GetCertSecretInfo(cfg CertificateProvisioningConfig) CertSecretInfo +} + +type CertSecretInfo struct { + SecretName string + CertificateKey string + PrivateKeyKey string +} diff --git a/internal/operator-controller/rukpak/convert/render/generate.go b/internal/operator-controller/rukpak/convert/render/generate.go index c3ebd9ffc..1bc01f892 100644 --- a/internal/operator-controller/rukpak/convert/render/generate.go +++ b/internal/operator-controller/rukpak/convert/render/generate.go @@ -22,6 +22,7 @@ type Options struct { InstallNamespace string TargetNamespaces []string UniqueNameGenerator UniqueNameGenerator + CertificateProvider CertificateProvider } type ResourceGenerator func(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) diff --git a/internal/operator-controller/rukpak/convert/render/mutate.go b/internal/operator-controller/rukpak/convert/render/mutate.go index 775442d79..9bc4df26d 100644 --- a/internal/operator-controller/rukpak/convert/render/mutate.go +++ b/internal/operator-controller/rukpak/convert/render/mutate.go @@ -3,8 +3,13 @@ package render import ( "iter" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" ) type ResourceMutator func(client.Object) error @@ -46,18 +51,18 @@ func (g *ResourceMutators) MutateObjects(objs iter.Seq[client.Object]) error { return nil } -type ResourceMutatorFactory func() (ResourceMutators, error) +type ResourceMutatorFactory func(rv1 *convert.RegistryV1, opts Options) (ResourceMutators, error) -func (m ResourceMutatorFactory) MakeResourceMutators() (ResourceMutators, error) { - return m() +func (m ResourceMutatorFactory) MakeResourceMutators(rv1 *convert.RegistryV1, opts Options) (ResourceMutators, error) { + return m(rv1, opts) } type ChainedResourceMutatorFactory []ResourceMutatorFactory -func (c ChainedResourceMutatorFactory) MakeResourceMutators() (ResourceMutators, error) { +func (c ChainedResourceMutatorFactory) MakeResourceMutators(rv1 *convert.RegistryV1, opts Options) (ResourceMutators, error) { var resourceMutators []ResourceMutator for _, mutatorFactory := range c { - mutators, err := mutatorFactory.MakeResourceMutators() + mutators, err := mutatorFactory.MakeResourceMutators(rv1, opts) if err != nil { return nil, err } @@ -75,3 +80,43 @@ func CustomResourceDefinitionMutator(name string, mutator func(crd *apiextension return mutator(crd) } } + +func ValidatingWebhookConfigurationMutator(name string, mutator func(wh *admissionregistrationv1.ValidatingWebhookConfiguration) error) ResourceMutator { + return func(obj client.Object) error { + wh, ok := obj.(*admissionregistrationv1.ValidatingWebhookConfiguration) + if !ok || wh.GetName() != name { + return nil + } + return mutator(wh) + } +} + +func MutatingWebhookConfigurationMutator(name string, mutator func(wh *admissionregistrationv1.MutatingWebhookConfiguration) error) ResourceMutator { + return func(obj client.Object) error { + wh, ok := obj.(*admissionregistrationv1.MutatingWebhookConfiguration) + if !ok || wh.GetName() != name { + return nil + } + return mutator(wh) + } +} + +func DeploymentResourceMutator(name string, namespace string, mutator func(dep *appsv1.Deployment) error) ResourceMutator { + return func(obj client.Object) error { + dep, ok := obj.(*appsv1.Deployment) + if !ok || dep.GetName() != name || dep.GetNamespace() != namespace { + return nil + } + return mutator(dep) + } +} + +func ServiceResourceMutator(name string, namespace string, mutator func(svc *corev1.Service) error) ResourceMutator { + return func(obj client.Object) error { + svc, ok := obj.(*corev1.Service) + if !ok || svc.GetName() != name || svc.GetNamespace() != namespace { + return nil + } + return mutator(svc) + } +} diff --git a/internal/operator-controller/rukpak/convert/render/render.go b/internal/operator-controller/rukpak/convert/render/render.go index 6c999bc8e..08582863a 100644 --- a/internal/operator-controller/rukpak/convert/render/render.go +++ b/internal/operator-controller/rukpak/convert/render/render.go @@ -16,6 +16,7 @@ type Option func(*options) type options struct { UniqueNameGenerator UniqueNameGenerator + CertificateProvider CertificateProvider } func (o *options) apply(opts ...Option) *options { @@ -25,6 +26,23 @@ func (o *options) apply(opts ...Option) *options { return o } +var PlainBundleRenderer = BundleRenderer{ + ResourceGenerators: []ResourceGenerator{ + BundleServiceAccountGenerator, + BundlePermissionsGenerator, + BundleClusterPermissionsGenerator, + BundleCRDGenerator, + BundleResourceGenerator, + BundleWebhookResourceGenerator, + CertProviderResourceGenerator, + BundleDeploymentGenerator, + }, + ResourceMutatorFactories: []ResourceMutatorFactory{ + BundleConversionWebhookResourceMutator, + CertificateProviderResourceMutator, + }, +} + type BundleRenderer struct { ResourceGenerators []ResourceGenerator ResourceMutatorFactories []ResourceMutatorFactory @@ -33,6 +51,7 @@ type BundleRenderer struct { func (r BundleRenderer) Render(rv1 convert.RegistryV1, installNamespace string, watchNamespaces []string, opts ...Option) ([]client.Object, error) { renderOptions := (&options{ UniqueNameGenerator: DefaultUniqueNameGenerator, + CertificateProvider: CertManagerProvider{}, }).apply(opts...) // create generation options @@ -40,10 +59,11 @@ func (r BundleRenderer) Render(rv1 convert.RegistryV1, installNamespace string, InstallNamespace: installNamespace, TargetNamespaces: watchNamespaces, UniqueNameGenerator: renderOptions.UniqueNameGenerator, + CertificateProvider: renderOptions.CertificateProvider, } // generate object mutators - objMutators, err := ChainedResourceMutatorFactory(r.ResourceMutatorFactories).MakeResourceMutators() + objMutators, err := ChainedResourceMutatorFactory(r.ResourceMutatorFactories).MakeResourceMutators(&rv1, genOpts) if err != nil { return nil, err } @@ -70,6 +90,5 @@ func DefaultUniqueNameGenerator(base string, o interface{}) (string, error) { if len(base)+len(hashStr) > maxNameLength { base = base[:maxNameLength-len(hashStr)-1] } - return fmt.Sprintf("%s-%s", base, hashStr), nil } diff --git a/internal/operator-controller/rukpak/convert/render/render_test.go b/internal/operator-controller/rukpak/convert/render/render_test.go index 606d0e8b2..5949bc563 100644 --- a/internal/operator-controller/rukpak/convert/render/render_test.go +++ b/internal/operator-controller/rukpak/convert/render/render_test.go @@ -46,7 +46,7 @@ func Test_BundleRenderer_CallsResourceMutators(t *testing.T) { }, }, ResourceMutatorFactories: []render.ResourceMutatorFactory{ - func() (render.ResourceMutators, error) { + func(rv1 *convert.RegistryV1, opts render.Options) (render.ResourceMutators, error) { return []render.ResourceMutator{ func(object client.Object) error { switch object.(type) { @@ -65,7 +65,7 @@ func Test_BundleRenderer_CallsResourceMutators(t *testing.T) { }, }, nil }, - func() (render.ResourceMutators, error) { + func(rv1 *convert.RegistryV1, opts render.Options) (render.ResourceMutators, error) { return []render.ResourceMutator{ func(object client.Object) error { object.SetAnnotations(map[string]string{ @@ -130,7 +130,7 @@ func Test_BundleRenderer_ReturnsResourceMutatorFactoryErrors(t *testing.T) { }, }, ResourceMutatorFactories: []render.ResourceMutatorFactory{ - func() (render.ResourceMutators, error) { + func(rv1 *convert.RegistryV1, opts render.Options) (render.ResourceMutators, error) { return nil, errors.New("mutator factory error") }, }, @@ -149,7 +149,7 @@ func Test_BundleRenderer_ReturnsResourceMutatorErrors(t *testing.T) { }, }, ResourceMutatorFactories: []render.ResourceMutatorFactory{ - func() (render.ResourceMutators, error) { + func(rv1 *convert.RegistryV1, opts render.Options) (render.ResourceMutators, error) { return []render.ResourceMutator{ func(object client.Object) error { return errors.New("mutator error") diff --git a/internal/operator-controller/rukpak/convert/render/resources.go b/internal/operator-controller/rukpak/convert/render/resources.go index 4664b02ac..61b03b14d 100644 --- a/internal/operator-controller/rukpak/convert/render/resources.go +++ b/internal/operator-controller/rukpak/convert/render/resources.go @@ -1,6 +1,7 @@ package render import ( + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -71,6 +72,33 @@ func WithLabels(labels map[string]string) func(client.Object) { } } +func WithServiceSpec(serviceSpec corev1.ServiceSpec) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *corev1.Service: + o.Spec = serviceSpec + } + } +} + +func WithValidatingWebhooks(webhooks ...admissionregistrationv1.ValidatingWebhook) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *admissionregistrationv1.ValidatingWebhookConfiguration: + o.Webhooks = webhooks + } + } +} + +func WithMutatingWebhooks(webhooks ...admissionregistrationv1.MutatingWebhook) func(client.Object) { + return func(obj client.Object) { + switch o := obj.(type) { + case *admissionregistrationv1.MutatingWebhookConfiguration: + o.Webhooks = webhooks + } + } +} + func GenerateServiceAccountResource(name string, namespace string, opts ...ResourceGenerationOption) *corev1.ServiceAccount { return ResourceGenerationOptions(opts).ApplyTo( &corev1.ServiceAccount{ @@ -158,3 +186,44 @@ func GenerateDeploymentResource(name string, namespace string, opts ...ResourceG }, ).(*appsv1.Deployment) } + +func GenerateValidatingWebhookConfigurationResource(name string, opts ...ResourceGenerationOption) *admissionregistrationv1.ValidatingWebhookConfiguration { + return ResourceGenerationOptions(opts).ApplyTo( + &admissionregistrationv1.ValidatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: "ValidatingWebhookConfiguration", + APIVersion: admissionregistrationv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, + ).(*admissionregistrationv1.ValidatingWebhookConfiguration) +} + +func GenerateMutatingWebhookConfigurationResource(name string, opts ...ResourceGenerationOption) *admissionregistrationv1.MutatingWebhookConfiguration { + return ResourceGenerationOptions(opts).ApplyTo( + &admissionregistrationv1.MutatingWebhookConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: "MutatingWebhookConfiguration", + APIVersion: admissionregistrationv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + }, + ).(*admissionregistrationv1.MutatingWebhookConfiguration) +} + +func GenerateServiceResource(name string, namespace string, opts ...ResourceGenerationOption) *corev1.Service { + return ResourceGenerationOptions(opts).ApplyTo(&corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }).(*corev1.Service) +} diff --git a/internal/operator-controller/rukpak/convert/render/webhooks.go b/internal/operator-controller/rukpak/convert/render/webhooks.go index 017111b03..180eff184 100644 --- a/internal/operator-controller/rukpak/convert/render/webhooks.go +++ b/internal/operator-controller/rukpak/convert/render/webhooks.go @@ -8,9 +8,9 @@ import ( "strings" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/controller-runtime/pkg/client" @@ -20,33 +20,6 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" ) -func WithServiceSpec(serviceSpec corev1.ServiceSpec) func(client.Object) { - return func(obj client.Object) { - switch o := obj.(type) { - case *corev1.Service: - o.Spec = serviceSpec - } - } -} - -func WithValidatingWebhooks(webhooks ...admissionregistrationv1.ValidatingWebhook) func(client.Object) { - return func(obj client.Object) { - switch o := obj.(type) { - case *admissionregistrationv1.ValidatingWebhookConfiguration: - o.Webhooks = webhooks - } - } -} - -func WithMutatingWebhooks(webhooks ...admissionregistrationv1.MutatingWebhook) func(client.Object) { - return func(obj client.Object) { - switch o := obj.(type) { - case *admissionregistrationv1.MutatingWebhookConfiguration: - o.Webhooks = webhooks - } - } -} - func BundleWebhookResourceGenerator(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { //nolint:prealloc var objs []client.Object @@ -203,45 +176,129 @@ func BundleConversionWebhookResourceMutator(rv1 *convert.RegistryV1, opts Option return mutators, nil } -func GenerateValidatingWebhookConfigurationResource(name string, opts ...ResourceGenerationOption) *admissionregistrationv1.ValidatingWebhookConfiguration { - return ResourceGenerationOptions(opts).ApplyTo( - &admissionregistrationv1.ValidatingWebhookConfiguration{ - TypeMeta: metav1.TypeMeta{ - Kind: "ValidatingWebhookConfiguration", - APIVersion: admissionregistrationv1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - }, - ).(*admissionregistrationv1.ValidatingWebhookConfiguration) -} +func CertificateProviderResourceMutator(rv1 *convert.RegistryV1, opts Options) (ResourceMutators, error) { + resourceMutators := ResourceMutators{} + webhookDefnsByDeployment := map[string][]v1alpha1.WebhookDescription{} -func GenerateMutatingWebhookConfigurationResource(name string, opts ...ResourceGenerationOption) *admissionregistrationv1.MutatingWebhookConfiguration { - return ResourceGenerationOptions(opts).ApplyTo( - &admissionregistrationv1.MutatingWebhookConfiguration{ - TypeMeta: metav1.TypeMeta{ - Kind: "MutatingWebhookConfiguration", - APIVersion: admissionregistrationv1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - }, - ).(*admissionregistrationv1.MutatingWebhookConfiguration) + for _, wh := range rv1.CSV.Spec.WebhookDefinitions { + webhookDefnsByDeployment[wh.DeploymentName] = append(webhookDefnsByDeployment[wh.DeploymentName], wh) + } + + for depName, webhooks := range webhookDefnsByDeployment { + certCfg := getCertCfgForDeployment(depName, opts.InstallNamespace, rv1.CSV.Name) + + resourceMutators.Append( + DeploymentResourceMutator(depName, opts.InstallNamespace, func(dep *appsv1.Deployment) error { + dep.Spec.Template.Spec.Volumes = slices.DeleteFunc(dep.Spec.Template.Spec.Volumes, func(v corev1.Volume) bool { + return v.Name == "apiservice-cert" || v.Name == "webhook-cert" + }) + dep.Spec.Template.Spec.Volumes = append(dep.Spec.Template.Spec.Volumes, + corev1.Volume{ + Name: "apiservice-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: certCfg.CertName, + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "apiserver.crt", + }, + { + Key: "tls.key", + Path: "apiserver.key", + }, + }, + }, + }, + }, + corev1.Volume{ + Name: "webhook-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: certCfg.CertName, + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "tls.crt", + }, + { + Key: "tls.key", + Path: "tls.key", + }, + }, + }, + }, + }, + ) + + volumeMounts := []corev1.VolumeMount{ + {Name: "apiservice-cert", MountPath: "/apiserver.local.config/certificates"}, + {Name: "webhook-cert", MountPath: "/tmp/k8s-webhook-server/serving-certs"}, + } + for i := range dep.Spec.Template.Spec.Containers { + dep.Spec.Template.Spec.Containers[i].VolumeMounts = slices.DeleteFunc(dep.Spec.Template.Spec.Containers[i].VolumeMounts, func(vm corev1.VolumeMount) bool { + return vm.Name == "apiservice-cert" || vm.Name == "webhook-cert" + }) + dep.Spec.Template.Spec.Containers[i].VolumeMounts = append(dep.Spec.Template.Spec.Containers[i].VolumeMounts, volumeMounts...) + } + + return nil + }), + ) + + resourceMutators.Append( + ServiceResourceMutator(getWebhookServiceName(depName), opts.InstallNamespace, func(svc *corev1.Service) error { + return opts.CertificateProvider.InjectCABundle(svc, certCfg) + }), + ) + + for _, wh := range webhooks { + switch wh.Type { + case v1alpha1.ValidatingAdmissionWebhook: + resourceMutators.Append( + ValidatingWebhookConfigurationMutator(wh.GenerateName, func(whResource *admissionregistrationv1.ValidatingWebhookConfiguration) error { + return opts.CertificateProvider.InjectCABundle(whResource, certCfg) + }), + ) + case v1alpha1.MutatingAdmissionWebhook: + resourceMutators.Append( + MutatingWebhookConfigurationMutator(wh.GenerateName, func(whResource *admissionregistrationv1.MutatingWebhookConfiguration) error { + return opts.CertificateProvider.InjectCABundle(whResource, certCfg) + }), + ) + case v1alpha1.ConversionWebhook: + for _, crdName := range wh.ConversionCRDs { + resourceMutators.Append( + CustomResourceDefinitionMutator(crdName, func(crd *apiextensionsv1.CustomResourceDefinition) error { + return opts.CertificateProvider.InjectCABundle(crd, certCfg) + }), + ) + } + } + } + } + return resourceMutators, nil } -func GenerateServiceResource(name string, namespace string, opts ...ResourceGenerationOption) *corev1.Service { - return ResourceGenerationOptions(opts).ApplyTo(&corev1.Service{ - TypeMeta: metav1.TypeMeta{ - Kind: "Service", - APIVersion: corev1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - }).(*corev1.Service) +func CertProviderResourceGenerator(rv1 *convert.RegistryV1, opts Options) ([]client.Object, error) { + var objs []client.Object + deploymentsWithWebhooks := sets.Set[string]{} + + for _, wh := range rv1.CSV.Spec.WebhookDefinitions { + deploymentsWithWebhooks.Insert(wh.DeploymentName) + } + + for _, depName := range deploymentsWithWebhooks.UnsortedList() { + certCfg := getCertCfgForDeployment(depName, opts.InstallNamespace, rv1.CSV.Name) + certObjs, err := opts.CertificateProvider.AdditionalObjects(certCfg) + if err != nil { + return nil, err + } + for _, certObj := range certObjs { + objs = append(objs, &certObj) + } + } + return objs, nil } func getWebhookServicePort(wh v1alpha1.WebhookDescription) corev1.ServicePort { @@ -265,3 +322,11 @@ func getWebhookServicePort(wh v1alpha1.WebhookDescription) corev1.ServicePort { func getWebhookServiceName(deploymentName string) string { return fmt.Sprintf("%s-service", strings.ReplaceAll(deploymentName, ".", "-")) } + +func getCertCfgForDeployment(deploymentName string, deploymentNamespace string, csvName string) CertificateProvisioningConfig { + return CertificateProvisioningConfig{ + WebhookServiceName: getWebhookServiceName(deploymentName), + Namespace: deploymentNamespace, + CertName: fmt.Sprintf("%s-%s-crt", csvName, deploymentName), + } +}