diff --git a/internal/operator-controller/rukpak/convert/registryv1_test.go b/internal/operator-controller/rukpak/convert/registryv1_test.go index 34f7722d0..8d6b90a29 100644 --- a/internal/operator-controller/rukpak/convert/registryv1_test.go +++ b/internal/operator-controller/rukpak/convert/registryv1_test.go @@ -14,16 +14,20 @@ import ( corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" schedulingv1 "k8s.io/api/scheduling/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/operator-framework/api/pkg/operators/v1alpha1" "github.com/operator-framework/operator-registry/alpha/property" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert" + . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" + filterutil "github.com/operator-framework/operator-controller/internal/shared/util/filter" ) const ( @@ -36,14 +40,7 @@ const ( ) func getCsvAndService() (v1alpha1.ClusterServiceVersion, corev1.Service) { - csv := v1alpha1.ClusterServiceVersion{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testCSV", - }, - Spec: v1alpha1.ClusterServiceVersionSpec{ - InstallModes: []v1alpha1.InstallMode{{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}}, - }, - } + csv := MakeCSV(WithName("testCSV"), WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)) svc := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "testService", @@ -213,49 +210,41 @@ func TestRegistryV1SuiteNamespaceClusterScoped(t *testing.T) { func getBaseCsvAndService() (v1alpha1.ClusterServiceVersion, corev1.Service) { // base CSV definition that each test case will deep copy and modify - baseCSV := v1alpha1.ClusterServiceVersion{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testCSV", - Annotations: map[string]string{ - olmProperties: fmt.Sprintf("[{\"type\": %s, \"value\": \"%s\"}]", property.TypeConstraint, "value"), - }, - }, - Spec: v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategySpec: v1alpha1.StrategyDetailsDeployment{ - DeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ - { - Name: "testDeployment", - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "testContainer", - Image: "testImage", - }, - }, - }, - }, - }, - }, - }, - Permissions: []v1alpha1.StrategyDeploymentPermissions{ - { - ServiceAccountName: "testServiceAccount", - Rules: []rbacv1.PolicyRule{ + baseCSV := MakeCSV( + WithName("testCSV"), + WithAnnotations(map[string]string{ + olmProperties: fmt.Sprintf("[{\"type\": %s, \"value\": \"%s\"}]", property.TypeConstraint, "value"), + }), + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "testDeployment", + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ { - APIGroups: []string{"test"}, - Resources: []string{"pods"}, - Verbs: []string{"*"}, + Name: "testContainer", + Image: "testImage", }, }, }, }, }, }, - }, - } + ), + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "testServiceAccount", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"test"}, + Resources: []string{"pods"}, + Verbs: []string{"*"}, + }, + }, + }, + ), + ) svc := corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -615,6 +604,1081 @@ func TestRegistryV1SuiteGenerateNoAPISerciceDefinitions(t *testing.T) { require.Nil(t, plainBundle) } +func Test_Convert_DeploymentResourceGeneration(t *testing.T) { + for _, tc := range []struct { + name string + bundle convert.RegistryV1 + installNamespace string + targetNamespaces []string + expectedResources []client.Object + }{ + { + name: "generates deployment resources", + installNamespace: "install-namespace", + targetNamespaces: []string{""}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), + 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", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "some-service-account", + }, + }, + }, + }, + v1alpha1.StrategyDeploymentSpec{ + Name: "deployment-two", + Spec: appsv1.DeploymentSpec{}, + }, + ), + ), + }, + 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": "", + "pod": "annotation", + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "some-service-account", + }, + }, + }, + }, + &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": "", + }, + }, + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + conv := convert.Converter{} + plain, err := conv.Convert(tc.bundle, tc.installNamespace, tc.targetNamespaces) + require.NoError(t, err) + for _, expectedObj := range tc.expectedResources { + // find object in generated objects + result := filterutil.Filter(plain.Objects, byTargetObject(expectedObj)) + require.Len(t, result, 1) + require.Equal(t, expectedObj, result[0]) + } + }) + } +} + +func Test_Convert_RoleRoleBindingResourceGeneration(t *testing.T) { + for _, tc := range []struct { + name string + installNamespace string + targetNamespaces []string + bundle convert.RegistryV1 + expectedResources []client.Object + }{ + { + name: "does not generate any resources when in AllNamespaces mode (target namespace is [''])", + installNamespace: "install-namespace", + targetNamespaces: []string{metav1.NamespaceAll}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), + 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)", + installNamespace: "install-namespace", + targetNamespaces: []string{"watch-namespace"}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithName("csv"), + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace), + 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-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + 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-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + 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-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + }, + }, + }, + { + name: "generates role and rolebinding for permission service-account for each target namespace when in MultiNamespace install mode (target namespace contains multiple namespaces)", + installNamespace: "install-namespace", + targetNamespaces: []string{"watch-namespace", "watch-namespace-two"}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeMultiNamespace), + 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-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + 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-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + 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-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + }, + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace-two", + Name: "csv-service-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + 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-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + 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-accoun-1qo4d10qruxvfkjf58b5cymauit0vk33tu31q7au0p6k", + }, + }, + }, + }, + { + name: "generates role and rolebinding for each permission service-account", + installNamespace: "install-namespace", + targetNamespaces: []string{"watch-namespace"}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeAllNamespaces), + 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--c1nyhtj4melkktv8nq58cczhkreg8wbfd2umv97vci", + }, + 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--c1nyhtj4melkktv8nq58cczhkreg8wbfd2umv97vci", + }, + 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--c1nyhtj4melkktv8nq58cczhkreg8wbfd2umv97vci", + }, + }, + &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "watch-namespace", + Name: "csv-service-account-d2x7x81lh02xpvfl0hrc7he83vd3svym7paq0oj39hk", + }, + 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-d2x7x81lh02xpvfl0hrc7he83vd3svym7paq0oj39hk", + }, + 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-d2x7x81lh02xpvfl0hrc7he83vd3svym7paq0oj39hk", + }, + }, + }, + }, + { + name: "treats empty service account as 'default' service account", + installNamespace: "install-namespace", + targetNamespaces: []string{"watch-namespace"}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeAllNamespaces), + 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-f0sf4spj31ti6476d21w12dcdo76i4alx2thty14vgc", + }, + 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-f0sf4spj31ti6476d21w12dcdo76i4alx2thty14vgc", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "default", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "csv-default-f0sf4spj31ti6476d21w12dcdo76i4alx2thty14vgc", + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + conv := convert.Converter{} + plain, err := conv.Convert(tc.bundle, tc.installNamespace, tc.targetNamespaces) + require.NoError(t, err) + for _, expectedObj := range tc.expectedResources { + // find object in generated objects + result := filterutil.Filter(plain.Objects, byTargetObject(expectedObj)) + require.Len(t, result, 1) + require.Equal(t, expectedObj, result[0]) + } + }) + } +} + +func Test_Convert_ClusterRoleClusterRoleBindingResourceGeneration(t *testing.T) { + for _, tc := range []struct { + name string + installNamespace string + targetNamespaces []string + bundle convert.RegistryV1 + expectedResources []client.Object + }{ + { + name: "promotes permissions to clusters permissions and adds namespace policy rule when in AllNamespaces mode (target namespace is [''])", + installNamespace: "install-namespace", + targetNamespaces: []string{""}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), + 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-accoun-183oadrm9kfo7nconyoo014k1ff8dy5v3u5lbom19pat", + }, + 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-accoun-183oadrm9kfo7nconyoo014k1ff8dy5v3u5lbom19pat", + }, + 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-accoun-183oadrm9kfo7nconyoo014k1ff8dy5v3u5lbom19pat", + }, + }, + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-vgd0bghvdoibjpu1pj6maoju7rfr1odnhm2ylfxtfh3", + }, + 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-vgd0bghvdoibjpu1pj6maoju7rfr1odnhm2ylfxtfh3", + }, + 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-vgd0bghvdoibjpu1pj6maoju7rfr1odnhm2ylfxtfh3", + }, + }, + }, + }, + { + name: "generates clusterroles and clusterrolebindings for clusterpermissions", + installNamespace: "install-namespace", + targetNamespaces: []string{"watch-namespace"}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace), + 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--c1nyhtj4melkktv8nq58cczhkreg8wbfd2umv97vci", + }, + 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--c1nyhtj4melkktv8nq58cczhkreg8wbfd2umv97vci", + }, + 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--c1nyhtj4melkktv8nq58cczhkreg8wbfd2umv97vci", + }, + }, + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "csv-service-account-d2x7x81lh02xpvfl0hrc7he83vd3svym7paq0oj39hk", + }, + 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-d2x7x81lh02xpvfl0hrc7he83vd3svym7paq0oj39hk", + }, + 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-d2x7x81lh02xpvfl0hrc7he83vd3svym7paq0oj39hk", + }, + }, + }, + }, + { + name: "treats empty service accounts as 'default' service account", + installNamespace: "install-namespace", + targetNamespaces: []string{"watch-namespace"}, + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace), + 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-f0sf4spj31ti6476d21w12dcdo76i4alx2thty14vgc", + }, + 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-f0sf4spj31ti6476d21w12dcdo76i4alx2thty14vgc", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: "", + Name: "default", + Namespace: "install-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "csv-default-f0sf4spj31ti6476d21w12dcdo76i4alx2thty14vgc", + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + conv := convert.Converter{} + plain, err := conv.Convert(tc.bundle, tc.installNamespace, tc.targetNamespaces) + require.NoError(t, err) + for _, expectedObj := range tc.expectedResources { + // find object in generated objects + result := filterutil.Filter(plain.Objects, byTargetObject(expectedObj)) + require.Len(t, result, 1) + require.Equal(t, expectedObj, result[0]) + } + }) + } +} + +func Test_Convert_ServiceAccountResourceGeneration(t *testing.T) { + for _, tc := range []struct { + name string + installNamespace string + targetNamespaces []string + bundle convert.RegistryV1 + expectedResources []client.Object + }{ + { + name: "generates unique set of clusterpermissions and permissions service accounts in the install namespace", + installNamespace: "install-namespace", + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), + 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", + installNamespace: "install-namespace", + bundle: convert.RegistryV1{ + CSV: MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces), + 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) { + conv := convert.Converter{} + plain, err := conv.Convert(tc.bundle, tc.installNamespace, tc.targetNamespaces) + require.NoError(t, err) + for _, expectedObj := range tc.expectedResources { + // find object in generated objects + result := filterutil.Filter(plain.Objects, byTargetObject(expectedObj)) + require.Len(t, result, 1) + require.Equal(t, expectedObj, result[0]) + } + }) + } +} + +func Test_Convert_BundleCRDGeneration(t *testing.T) { + bundle := convert.RegistryV1{ + CRDs: []apiextensionsv1.CustomResourceDefinition{ + {ObjectMeta: metav1.ObjectMeta{Name: "crd-one"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "crd-two"}}, + }, + CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + } + + conv := convert.Converter{} + plain, err := conv.Convert(bundle, "install-namespace", []string{""}) + require.NoError(t, err) + expectedResources := []client.Object{ + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "crd-one"}}, + &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: "crd-two"}}, + } + + for _, expectedObj := range expectedResources { + // find object in generated objects + result := filterutil.Filter(plain.Objects, byTargetObject(expectedObj)) + require.Len(t, result, 1) + require.Equal(t, expectedObj, result[0]) + } +} + +func Test_Convert_AdditionalResourcesGeneration(t *testing.T) { + bundle := convert.RegistryV1{ + CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), + Others: []unstructured.Unstructured{ + convertToUnstructured(t, + &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bundled-service", + }, + }, + ), + convertToUnstructured(t, + &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bundled-clusterrole", + }, + }, + ), + }, + } + + conv := convert.Converter{} + plain, err := conv.Convert(bundle, "install-namespace", []string{""}) + require.NoError(t, err) + expectedResources := []unstructured.Unstructured{ + convertToUnstructured(t, &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bundled-service", + Namespace: "install-namespace", + }, + }), + convertToUnstructured(t, &rbacv1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterRole", + APIVersion: rbacv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bundled-clusterrole", + }, + }), + } + + for _, expectedObj := range expectedResources { + // find object in generated objects + result := filterutil.Filter(plain.Objects, byTargetObject(&expectedObj)) + require.Len(t, result, 1) + require.Equal(t, &expectedObj, result[0]) + } +} + func convertToUnstructured(t *testing.T, obj interface{}) unstructured.Unstructured { unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj) require.NoError(t, err) @@ -659,6 +1723,14 @@ spec: } } +func byTargetObject(obj client.Object) filterutil.Predicate[client.Object] { + return func(entity client.Object) bool { + return entity.GetName() == obj.GetName() && + entity.GetNamespace() == obj.GetNamespace() && + entity.GetObjectKind().GroupVersionKind() == obj.GetObjectKind().GroupVersionKind() + } +} + func removePaths(mapFs fstest.MapFS, paths ...string) fstest.MapFS { for k := range mapFs { for _, path := range paths { diff --git a/internal/operator-controller/rukpak/util/testing/testing.go b/internal/operator-controller/rukpak/util/testing/testing.go new file mode 100644 index 000000000..4a247dc07 --- /dev/null +++ b/internal/operator-controller/rukpak/util/testing/testing.go @@ -0,0 +1,72 @@ +package testing + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +type CSVOption func(version *v1alpha1.ClusterServiceVersion) + +//nolint:unparam +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 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 WithOwnedCRDs(crdDesc ...v1alpha1.CRDDescription) CSVOption { + return func(csv *v1alpha1.ClusterServiceVersion) { + csv.Spec.CustomResourceDefinitions.Owned = crdDesc + } +} + +func WithInstallModeSupportFor(installModeType ...v1alpha1.InstallModeType) CSVOption { + return func(csv *v1alpha1.ClusterServiceVersion) { + installModes := make([]v1alpha1.InstallMode, 0, len(installModeType)) + for _, t := range installModeType { + installModes = append(installModes, v1alpha1.InstallMode{ + Type: t, + Supported: true, + }) + } + csv.Spec.InstallModes = installModes + } +} + +func MakeCSV(opts ...CSVOption) v1alpha1.ClusterServiceVersion { + csv := v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: "ClusterServiceVersion", + }, + } + for _, opt := range opts { + opt(&csv) + } + return csv +} diff --git a/internal/operator-controller/rukpak/util/testing/testing_test.go b/internal/operator-controller/rukpak/util/testing/testing_test.go new file mode 100644 index 000000000..c8744cfa0 --- /dev/null +++ b/internal/operator-controller/rukpak/util/testing/testing_test.go @@ -0,0 +1,215 @@ +package testing_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + + . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" +) + +func Test_MakeCSV(t *testing.T) { + csv := MakeCSV() + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + }, csv) +} + +func Test_MakeCSV_WithName(t *testing.T) { + csv := MakeCSV(WithName("some-name")) + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + }, + }, csv) +} + +func Test_MakeCSV_WithStrategyDeploymentSpecs(t *testing.T) { + csv := MakeCSV( + WithStrategyDeploymentSpecs( + v1alpha1.StrategyDeploymentSpec{ + Name: "spec-one", + }, + v1alpha1.StrategyDeploymentSpec{ + Name: "spec-two", + }, + ), + ) + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategySpec: v1alpha1.StrategyDetailsDeployment{ + DeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ + { + Name: "spec-one", + }, + { + Name: "spec-two", + }, + }, + }, + }, + }, + }, csv) +} + +func Test_MakeCSV_WithPermissions(t *testing.T) { + csv := MakeCSV( + WithPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "", + }, + ), + ) + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategySpec: v1alpha1.StrategyDetailsDeployment{ + Permissions: []v1alpha1.StrategyDeploymentPermissions{ + { + ServiceAccountName: "service-account", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"list", "watch"}, + }, + }, + }, + { + ServiceAccountName: "", + }, + }, + }, + }, + }, + }, csv) +} + +func Test_MakeCSV_WithClusterPermissions(t *testing.T) { + csv := MakeCSV( + WithClusterPermissions( + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "service-account", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"list", "watch"}, + }, + }, + }, + v1alpha1.StrategyDeploymentPermissions{ + ServiceAccountName: "", + }, + ), + ) + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategySpec: v1alpha1.StrategyDetailsDeployment{ + ClusterPermissions: []v1alpha1.StrategyDeploymentPermissions{ + { + ServiceAccountName: "service-account", + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"list", "watch"}, + }, + }, + }, + { + ServiceAccountName: "", + }, + }, + }, + }, + }, + }, csv) +} + +func Test_MakeCSV_WithOwnedCRDs(t *testing.T) { + csv := MakeCSV( + WithOwnedCRDs( + v1alpha1.CRDDescription{Name: "a.crd.something"}, + v1alpha1.CRDDescription{Name: "b.crd.something"}, + ), + ) + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ + Owned: []v1alpha1.CRDDescription{ + {Name: "a.crd.something"}, + {Name: "b.crd.something"}, + }, + }, + }, + }, csv) +} + +func Test_MakeCSV_WithInstallModeSupportFor(t *testing.T) { + csv := MakeCSV( + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace), + ) + + require.Equal(t, v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterServiceVersion", + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallModes: []v1alpha1.InstallMode{ + { + Type: v1alpha1.InstallModeTypeAllNamespaces, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeSingleNamespace, + Supported: true, + }, + }, + }, + }, csv) +}