diff --git a/api/v1alpha1/clusterextension_types.go b/api/v1alpha1/clusterextension_types.go index c9760f958..a784ddd4a 100644 --- a/api/v1alpha1/clusterextension_types.go +++ b/api/v1alpha1/clusterextension_types.go @@ -83,6 +83,19 @@ type ClusterExtensionSpec struct { //+kubebuilder:Optional // Preflight defines the configuration of preflight checks. Preflight *PreflightConfig `json:"preflight,omitempty"` + + // ServiceAccount is used to install and manage resources. + // The service account is expected to exist in the InstallNamespace. + ServiceAccount ServiceAccountReference `json:"serviceAccount"` +} + +// ServiceAccountReference references a serviceAccount. +type ServiceAccountReference struct { + // name is the metadata.name of the referenced serviceAccount object. + //+kubebuilder:validation:MaxLength:=253 + //+kubebuilder:validation:Pattern:=^[a-z0-9]+([.|-][a-z0-9]+)*$ + //+kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable" + Name string `json:"name"` } // PreflightConfig holds the configuration for the preflight checks. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6dddf8b1d..acf701c7c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -122,6 +122,7 @@ func (in *ClusterExtensionSpec) DeepCopyInto(out *ClusterExtensionSpec) { *out = new(PreflightConfig) (*in).DeepCopyInto(*out) } + out.ServiceAccount = in.ServiceAccount } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionSpec. @@ -185,3 +186,18 @@ func (in *PreflightConfig) DeepCopy() *PreflightConfig { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountReference) DeepCopyInto(out *ServiceAccountReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountReference. +func (in *ServiceAccountReference) DeepCopy() *ServiceAccountReference { + if in == nil { + return nil + } + out := new(ServiceAccountReference) + in.DeepCopyInto(out) + return out +} diff --git a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml index 6331068a5..9b92a3045 100644 --- a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml +++ b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml @@ -72,6 +72,23 @@ spec: type: boolean type: object type: object + serviceAccount: + description: |- + ServiceAccount is used to install and manage resources. + The service account is expected to exist in the InstallNamespace. + properties: + name: + description: name is the metadata.name of the referenced serviceAccount + object. + maxLength: 253 + pattern: ^[a-z0-9]+([.|-][a-z0-9]+)*$ + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + required: + - name + type: object upgradeConstraintPolicy: default: Enforce description: Defines the policy for how to handle upgrade constraints @@ -93,6 +110,7 @@ spec: required: - installNamespace - packageName + - serviceAccount type: object status: description: ClusterExtensionStatus defines the observed state of ClusterExtension diff --git a/config/samples/olm_v1alpha1_clusterextension.yaml b/config/samples/olm_v1alpha1_clusterextension.yaml index 483ef34f1..475f3eeaf 100644 --- a/config/samples/olm_v1alpha1_clusterextension.yaml +++ b/config/samples/olm_v1alpha1_clusterextension.yaml @@ -6,3 +6,5 @@ spec: installNamespace: default packageName: argocd-operator version: 0.6.0 + serviceAccount: + name: argocd-installer diff --git a/internal/controllers/clusterextension_admission_test.go b/internal/controllers/clusterextension_admission_test.go index 16629c410..10c0bdb25 100644 --- a/internal/controllers/clusterextension_admission_test.go +++ b/internal/controllers/clusterextension_admission_test.go @@ -45,6 +45,9 @@ func TestClusterExtensionAdmissionPackageName(t *testing.T) { err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ PackageName: tc.pkgName, InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, })) if tc.errMsg == "" { require.NoError(t, err, "unexpected error for package name %q: %w", tc.pkgName, err) @@ -134,6 +137,9 @@ func TestClusterExtensionAdmissionVersion(t *testing.T) { PackageName: "package", Version: tc.version, InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, })) if tc.errMsg == "" { require.NoError(t, err, "unexpected error for version %q: %w", tc.version, err) @@ -179,6 +185,9 @@ func TestClusterExtensionAdmissionChannel(t *testing.T) { PackageName: "package", Channel: tc.channelName, InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, })) if tc.errMsg == "" { require.NoError(t, err, "unexpected error for channel %q: %w", tc.channelName, err) @@ -224,6 +233,9 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) { err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ PackageName: "package", InstallNamespace: tc.installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, })) if tc.errMsg == "" { require.NoError(t, err, "unexpected error for installNamespace %q: %w", tc.installNamespace, err) @@ -235,6 +247,54 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) { } } +func TestClusterExtensionAdmissionServiceAccount(t *testing.T) { + tooLongError := "spec.serviceAccount.name: Too long: may not be longer than 253" + regexMismatchError := "spec.serviceAccount.name in body should match" + + testCases := []struct { + name string + serviceAccount string + errMsg string + }{ + {"just alphanumeric", "justalphanumeric1", ""}, + {"hypen-separated", "hyphenated-name", ""}, + {"dot-separated", "dotted.name", ""}, + {"longest valid service account name", strings.Repeat("x", 253), ""}, + {"too long service account name", strings.Repeat("x", 254), tooLongError}, + {"no service account name", "", regexMismatchError}, + {"spaces", "spaces spaces", regexMismatchError}, + {"capitalized", "Capitalized", regexMismatchError}, + {"camel case", "camelCase", regexMismatchError}, + {"invalid characters", "many/invalid$characters+in_name", regexMismatchError}, + {"starts with hyphen", "-start-with-hyphen", regexMismatchError}, + {"ends with hyphen", "end-with-hyphen-", regexMismatchError}, + {"starts with period", ".start-with-period", regexMismatchError}, + {"ends with period", "end-with-period.", regexMismatchError}, + } + + t.Parallel() + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cl := newClient(t) + err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ + PackageName: "package", + InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: tc.serviceAccount, + }, + })) + if tc.errMsg == "" { + require.NoError(t, err, "unexpected error for service account name %q: %w", tc.serviceAccount, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } + }) + } +} + func buildClusterExtension(spec ocv1alpha1.ClusterExtensionSpec) *ocv1alpha1.ClusterExtension { return &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controllers/clusterextension_controller_test.go b/internal/controllers/clusterextension_controller_test.go index 09608afbb..a2adf08c9 100644 --- a/internal/controllers/clusterextension_controller_test.go +++ b/internal/controllers/clusterextension_controller_test.go @@ -56,6 +56,9 @@ func TestClusterExtensionNonExistentPackage(t *testing.T) { Spec: ocv1alpha1.ClusterExtensionSpec{ PackageName: pkgName, InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -98,6 +101,9 @@ func TestClusterExtensionNonExistentVersion(t *testing.T) { PackageName: pkgName, Version: "0.50.0", // this version of the package does not exist InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -147,6 +153,7 @@ func TestClusterExtensionChannelVersionExists(t *testing.T) { pkgVer := "1.0.0" pkgChan := "beta" installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, @@ -155,6 +162,9 @@ func TestClusterExtensionChannelVersionExists(t *testing.T) { Version: pkgVer, Channel: pkgChan, InstallNamespace: installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: serviceAccount, + }, }, } err := cl.Create(ctx, clusterExtension) @@ -206,6 +216,8 @@ func TestClusterExtensionChannelExistsNoVersion(t *testing.T) { pkgVer := "" pkgChan := "beta" installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) + clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ @@ -213,6 +225,9 @@ func TestClusterExtensionChannelExistsNoVersion(t *testing.T) { Version: pkgVer, Channel: pkgChan, InstallNamespace: installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: serviceAccount, + }, }, } err := cl.Create(ctx, clusterExtension) @@ -265,6 +280,9 @@ func TestClusterExtensionVersionNoChannel(t *testing.T) { Version: pkgVer, Channel: pkgChan, InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -313,6 +331,9 @@ func TestClusterExtensionNoChannel(t *testing.T) { PackageName: pkgName, Channel: pkgChan, InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -363,6 +384,9 @@ func TestClusterExtensionNoVersion(t *testing.T) { Version: pkgVer, Channel: pkgChan, InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -443,6 +467,7 @@ func TestClusterExtensionUpgrade(t *testing.T) { pkgVer := "1.0.0" pkgChan := "beta" installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, @@ -451,6 +476,9 @@ func TestClusterExtensionUpgrade(t *testing.T) { Version: pkgVer, Channel: pkgChan, InstallNamespace: installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: serviceAccount, + }, }, } // Create a cluster extension @@ -543,6 +571,7 @@ func TestClusterExtensionUpgrade(t *testing.T) { pkgVer := "1.0.0" pkgChan := "beta" installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, @@ -551,6 +580,9 @@ func TestClusterExtensionUpgrade(t *testing.T) { Version: pkgVer, Channel: pkgChan, InstallNamespace: installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: serviceAccount, + }, }, } // Create a cluster extension @@ -654,6 +686,7 @@ func TestClusterExtensionUpgrade(t *testing.T) { }() installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, @@ -663,6 +696,9 @@ func TestClusterExtensionUpgrade(t *testing.T) { Channel: "beta", UpgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, InstallNamespace: installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: serviceAccount, + }, }, } // Create a cluster extension @@ -754,6 +790,7 @@ func TestClusterExtensionDowngrade(t *testing.T) { }() installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, @@ -762,6 +799,9 @@ func TestClusterExtensionDowngrade(t *testing.T) { Version: "1.0.1", Channel: "beta", InstallNamespace: installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: serviceAccount, + }, }, } // Create a cluster extension @@ -842,6 +882,7 @@ func TestClusterExtensionDowngrade(t *testing.T) { }() installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, @@ -851,6 +892,9 @@ func TestClusterExtensionDowngrade(t *testing.T) { Channel: "beta", UpgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, InstallNamespace: installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: serviceAccount, + }, }, } // Create a cluster extension @@ -1461,6 +1505,9 @@ func TestClusterExtensionErrorGettingBundles(t *testing.T) { Spec: ocv1alpha1.ClusterExtensionSpec{ PackageName: "prometheus", InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, }, } require.NoError(t, cl.Create(ctx, clusterExtension)) diff --git a/internal/controllers/clusterextension_registryv1_validation_test.go b/internal/controllers/clusterextension_registryv1_validation_test.go index 83b0157f1..e041926a4 100644 --- a/internal/controllers/clusterextension_registryv1_validation_test.go +++ b/internal/controllers/clusterextension_registryv1_validation_test.go @@ -121,12 +121,16 @@ func TestClusterExtensionRegistryV1DisallowDependencies(t *testing.T) { } installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ PackageName: tt.bundle.Package, InstallNamespace: installNamespace, + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: serviceAccount, + }, }, } require.NoError(t, cl.Create(ctx, clusterExtension)) diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go index b4260efb3..3d3162e11 100644 --- a/test/e2e/cluster_extension_install_test.go +++ b/test/e2e/cluster_extension_install_test.go @@ -75,6 +75,9 @@ func TestClusterExtensionInstallRegistry(t *testing.T) { clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ PackageName: "prometheus", InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, } t.Log("It resolves the specified package with correct bundle path") t.Log("By creating the ClusterExtension resource") @@ -132,6 +135,9 @@ func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) { clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ PackageName: pkgName, InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, } t.Log("By deleting the catalog first") @@ -199,6 +205,9 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { PackageName: "prometheus", Version: "1.0.0", InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, } require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful installation") @@ -245,6 +254,9 @@ func TestClusterExtensionForceInstallNonSuccessorVersion(t *testing.T) { PackageName: "prometheus", Version: "1.0.0", InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, } require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful resolution") @@ -290,6 +302,9 @@ func TestClusterExtensionInstallSuccessorVersion(t *testing.T) { PackageName: "prometheus", Version: "1.0.0", InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, } require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful resolution") diff --git a/test/extension-developer-e2e/extension_developer_test.go b/test/extension-developer-e2e/extension_developer_test.go index 7de5f3fd7..107e15206 100644 --- a/test/extension-developer-e2e/extension_developer_test.go +++ b/test/extension-developer-e2e/extension_developer_test.go @@ -39,6 +39,9 @@ func TestExtensionDeveloper(t *testing.T) { Spec: ocv1alpha1.ClusterExtensionSpec{ PackageName: os.Getenv("REG_PKG_NAME"), InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, }, }, }