diff --git a/docs/draft/howto/single-ownnamespace-install.md b/docs/draft/howto/single-ownnamespace-install.md index fc36de0e7..8e6f00fe4 100644 --- a/docs/draft/howto/single-ownnamespace-install.md +++ b/docs/draft/howto/single-ownnamespace-install.md @@ -56,11 +56,43 @@ kubectl rollout status -n olmv1-system deployment/operator-controller-controller ## Configuring the `ClusterExtension` A `ClusterExtension` can be configured to install bundle in `Single-` or `OwnNamespace` mode through the -`.spec.config.inline.watchNamespace` property. The *installMode* is inferred in the following way: - - - *AllNamespaces*: `watchNamespace` is empty, or not set - - *OwnNamespace*: `watchNamespace` is the install namespace (i.e. `.spec.namespace`) - - *SingleNamespace*: `watchNamespace` *not* the install namespace +`.spec.config.inline.watchNamespace` property which may or may not be present or required depending on the bundle's +install mode support, if the bundle: + + - only supports *AllNamespaces* mode => `watchNamespace` is not a configuration + - supports *AllNamespaces* and *SingleNamespace* and/or *OwnNamespace* => `watchNamespace` is optional + - bundle only supports *SingleNamespace* and/or *OwnNamespace* => `watchNamespace` is required + +The `watchNamespace` configuration can only be the install namespace if the bundle supports the *OwnNamespace* install mode, and +it can only be any other namespace if the bundle supports the *SingleNamespace* install mode. + +Examples: + +Bundle only supports *AllNamespaces*: +- `watchNamespace` is not a configuration +- bundle will be installed in *AllNamespaces* mode + +Bundle only supports *OwnNamespace*: +- `watchNamespace` is required +- `watchNamespace` must be the install namespace +- bundle will always be installed in *OwnNamespace* mode + +Bundle supports *AllNamespace* and *OwnNamespace*: +- `watchNamespace` is optional +- if `watchNamespace` = install namespace => bundle will be installed in *OwnNamespace* mode +- if `watchNamespace` is null or not set => bundle will be installed in *AllNamespaces* mode +- if `watchNamespace` != install namespace => error + +Bundle only supports *SingleNamespace*: +- `watchNamespace` is required +- `watchNamespace` must *NOT* be the install namespace +- bundle will always be installed in *SingleNamespace* mode + +Bundle supports *AllNamespaces*, *SingleNamespace*, and *OwnNamespace* install modes: +- `watchNamespace` can be optionally configured +- if `watchNamespace` = install namespace => bundle will be installed in *OwnNamespace* mode +- if `watchNamespace` != install namespace => bundle will be installed in *SingleNamespace* mode +- if `watchNamespace` is null or not set => bundle will be installed in *AllNamespaces* mode ### Examples diff --git a/internal/operator-controller/applier/provider.go b/internal/operator-controller/applier/provider.go index f425e7415..ffb5eb559 100644 --- a/internal/operator-controller/applier/provider.go +++ b/internal/operator-controller/applier/provider.go @@ -68,8 +68,14 @@ func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtens render.WithCertificateProvider(r.CertificateProvider), } - if r.IsSingleOwnNamespaceEnabled && ext.Spec.Config != nil && ext.Spec.Config.ConfigType == ocv1.ClusterExtensionConfigTypeInline { - bundleConfig, err := bundle.UnmarshallConfig(ext.Spec.Config.Inline.Raw, rv1, ext.Spec.Namespace) + if r.IsSingleOwnNamespaceEnabled { + bundleConfigBytes := extensionConfigBytes(ext) + // treat no config as empty to properly validate the configuration + // e.g. ensure that validation catches missing required fields + if bundleConfigBytes == nil { + bundleConfigBytes = []byte(`{}`) + } + bundleConfig, err := bundle.UnmarshalConfig(bundleConfigBytes, rv1, ext.Spec.Namespace) if err != nil { return nil, fmt.Errorf("invalid bundle configuration: %w", err) } @@ -128,3 +134,17 @@ func (r *RegistryV1HelmChartProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExten return chrt, nil } + +// ExtensionConfigBytes returns the ClusterExtension configuration input by the user +// through .spec.config as a byte slice. +func extensionConfigBytes(ext *ocv1.ClusterExtension) []byte { + if ext.Spec.Config != nil { + switch ext.Spec.Config.ConfigType { + case ocv1.ClusterExtensionConfigTypeInline: + if ext.Spec.Config.Inline != nil { + return ext.Spec.Config.Inline.Raw + } + } + } + return nil +} diff --git a/internal/operator-controller/applier/provider_test.go b/internal/operator-controller/applier/provider_test.go index b1a6cd4f4..4ec20bead 100644 --- a/internal/operator-controller/applier/provider_test.go +++ b/internal/operator-controller/applier/provider_test.go @@ -244,15 +244,6 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) { t.Run("rejects bundles without AllNamespaces install mode and with SingleNamespace support when Single/OwnNamespace install mode support is enabled", func(t *testing.T) { expectedWatchNamespace := "some-namespace" provider := applier.RegistryV1ManifestProvider{ - BundleRenderer: render.BundleRenderer{ - ResourceGenerators: []render.ResourceGenerator{ - func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { - t.Log("ensure watch namespace is appropriately configured") - require.Equal(t, []string{expectedWatchNamespace}, opts.TargetNamespaces) - return nil, nil - }, - }, - }, IsSingleOwnNamespaceEnabled: false, } @@ -289,7 +280,7 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) { require.Contains(t, err.Error(), "unsupported bundle") }) - t.Run("accepts bundles without AllNamespaces install mode and with SingleNamespace support when Single/OwnNamespace install mode support is enabled", func(t *testing.T) { + t.Run("accepts bundles with install modes {SingleNamespace} when the appropriate configuration is given", func(t *testing.T) { expectedWatchNamespace := "some-namespace" provider := applier.RegistryV1ManifestProvider{ BundleRenderer: render.BundleRenderer{ @@ -321,20 +312,89 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) { require.NoError(t, err) }) - t.Run("accepts bundles without AllNamespaces install mode and with OwnNamespace support when Single/OwnNamespace install mode support is enabled", func(t *testing.T) { + t.Run("rejects bundles with {SingleNamespace} install modes when no configuration is given", func(t *testing.T) { provider := applier.RegistryV1ManifestProvider{ IsSingleOwnNamespaceEnabled: true, } + bundleFS := bundlefs.Builder().WithPackageName("test"). - WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build()).Build() + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ Spec: ocv1.ClusterExtensionSpec{ Namespace: "install-namespace", }, }) + require.Error(t, err) + require.Contains(t, err.Error(), "required field \"watchNamespace\" is missing") + }) + + t.Run("accepts bundles with {OwnNamespace} install modes when the appropriate configuration is given", func(t *testing.T) { + installNamespace := "some-namespace" + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + t.Log("ensure watch namespace is appropriately configured") + require.Equal(t, []string{installNamespace}, opts.TargetNamespaces) + return nil, nil + }, + }, + }, + IsSingleOwnNamespaceEnabled: true, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: installNamespace, + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "` + installNamespace + `"}`), + }, + }, + }, + }) require.NoError(t, err) }) + t.Run("rejects bundles with {OwnNamespace} install modes when no configuration is given", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: true, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "required field \"watchNamespace\" is missing") + }) + + t.Run("rejects bundles with {OwnNamespace} install modes when watchNamespace is not install namespace", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: true, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace).Build()).Build() + _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "not-install-namespace"}`), + }, + }, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid 'watchNamespace' \"not-install-namespace\": must be install namespace (install-namespace)") + }) + t.Run("rejects bundles without AllNamespaces, SingleNamespace, or OwnNamespace install mode support when Single/OwnNamespace install mode support is enabled", func(t *testing.T) { provider := applier.RegistryV1ManifestProvider{ IsSingleOwnNamespaceEnabled: true, diff --git a/internal/operator-controller/rukpak/bundle/config.go b/internal/operator-controller/rukpak/bundle/config.go index c65a2311a..7ec4bfdf0 100644 --- a/internal/operator-controller/rukpak/bundle/config.go +++ b/internal/operator-controller/rukpak/bundle/config.go @@ -17,20 +17,20 @@ type Config struct { WatchNamespace *string `json:"watchNamespace"` } -// UnmarshallConfig returns a deserialized and validated *bundle.Config based on bytes and validated +// UnmarshalConfig returns a deserialized *bundle.Config based on bytes and validated // against rv1 and the desired install namespaces. It will error if: // - rv is nil // - bytes is not a valid YAML/JSON object // - bytes is a valid YAML/JSON object but does not follow the registry+v1 schema -// if bytes is nil a nil bundle.Config is returned -func UnmarshallConfig(bytes []byte, rv1 RegistryV1, installNamespace string) (*Config, error) { +// - if bytes is nil, a nil *bundle.Config is returned with no error +func UnmarshalConfig(bytes []byte, rv1 RegistryV1, installNamespace string) (*Config, error) { if bytes == nil { return nil, nil } bundleConfig := &Config{} if err := yaml.UnmarshalStrict(bytes, bundleConfig); err != nil { - return nil, fmt.Errorf("error unmarshalling registry+v1 configuration: %w", formatUnmarshallError(err)) + return nil, fmt.Errorf("error unmarshalling registry+v1 configuration: %w", formatUnmarshalError(err)) } // collect bundle install modes @@ -83,24 +83,23 @@ func validateConfig(config *Config, installNamespace string, bundleInstallModeSe } // isWatchNamespaceConfigSupported returns true when the bundle exposes a watchNamespace configuration. This happens when: -// - SingleNamespace install more is supported, or -// - OwnNamespace and AllNamespaces install modes are supported +// - SingleNamespace and/or OwnNamespace install modes are supported func isWatchNamespaceConfigSupported(bundleInstallModeSet sets.Set[v1alpha1.InstallMode]) bool { - return bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}) || - bundleInstallModeSet.HasAll( - v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}, - v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}) + return bundleInstallModeSet.HasAny( + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}, + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}, + ) } // isWatchNamespaceConfigRequired returns true if the watchNamespace configuration is required. This happens when -// AllNamespaces install mode is not supported and SingleNamespace is supported +// AllNamespaces install mode is not supported and SingleNamespace and/or OwnNamespace is supported func isWatchNamespaceConfigRequired(bundleInstallModeSet sets.Set[v1alpha1.InstallMode]) bool { - return !bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}) && - bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}) + return isWatchNamespaceConfigSupported(bundleInstallModeSet) && + !bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}) } -// formatUnmarshallError format JSON unmarshal errors to be more readable -func formatUnmarshallError(err error) error { +// formatUnmarshalError format JSON unmarshal errors to be more readable +func formatUnmarshalError(err error) error { var unmarshalErr *json.UnmarshalTypeError if errors.As(err, &unmarshalErr) { if unmarshalErr.Field == "" { diff --git a/internal/operator-controller/rukpak/bundle/config_test.go b/internal/operator-controller/rukpak/bundle/config_test.go index 10494f651..a6c4b394a 100644 --- a/internal/operator-controller/rukpak/bundle/config_test.go +++ b/internal/operator-controller/rukpak/bundle/config_test.go @@ -12,7 +12,7 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" ) -func Test_UnmarshallConfig(t *testing.T) { +func Test_UnmarshalConfig(t *testing.T) { for _, tc := range []struct { name string rawConfig []byte @@ -22,7 +22,7 @@ func Test_UnmarshallConfig(t *testing.T) { expectedConfig *bundle.Config }{ { - name: "accepts nil raw config", + name: "returns nil for nil config", rawConfig: nil, expectedConfig: nil, }, @@ -91,16 +91,28 @@ func Test_UnmarshallConfig(t *testing.T) { expectedErrMessage: "unknown field \"watchNamespace\"", }, { - name: "reject with unknown field when install modes {OwnNamespace}", + name: "reject with required field when install modes {OwnNamespace} and watchNamespace is null", supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace}, - rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), - expectedErrMessage: "unknown field \"watchNamespace\"", + rawConfig: []byte(`{"watchNamespace": null}`), + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "reject with required field when install modes {OwnNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{}`), + expectedErrMessage: "required field \"watchNamespace\" is missing", }, { - name: "reject with unknown field when install modes {MultiNamespace, OwnNamespace}", + name: "reject with required field when install modes {MultiNamespace, OwnNamespace} and watchNamespace is null", supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeOwnNamespace}, - rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), - expectedErrMessage: "unknown field \"watchNamespace\"", + rawConfig: []byte(`{"watchNamespace": null}`), + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "reject with required field when install modes {MultiNamespace, OwnNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{}`), + expectedErrMessage: "required field \"watchNamespace\" is missing", }, { name: "accepts when install modes {SingleNamespace} and watchNamespace != install namespace", @@ -202,6 +214,27 @@ func Test_UnmarshallConfig(t *testing.T) { installNamespace: "not-some-namespace", expectedErrMessage: "required field \"watchNamespace\" is missing", }, + { + name: "rejects with required field error when install modes {SingleNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, OwnNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, MultiNamespace} and watchNamespace is missing", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, { name: "rejects with required field error when install modes {SingleNamespace, OwnNamespace, MultiNamespace} and watchNamespace is nil", supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, @@ -227,6 +260,24 @@ func Test_UnmarshallConfig(t *testing.T) { WatchNamespace: nil, }, }, + { + name: "accepts no watchNamespace when install modes {AllNamespaces, OwnNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: nil, + }, + }, + { + name: "accepts no watchNamespace when install modes {AllNamespaces, OwnNamespace, MultiNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{}`), + installNamespace: "not-some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: nil, + }, + }, { name: "rejects with format error when install modes are {SingleNamespace, OwnNamespace} and watchNamespace is ''", supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, @@ -246,7 +297,7 @@ func Test_UnmarshallConfig(t *testing.T) { } } - config, err := bundle.UnmarshallConfig(tc.rawConfig, rv1, tc.installNamespace) + config, err := bundle.UnmarshalConfig(tc.rawConfig, rv1, tc.installNamespace) require.Equal(t, tc.expectedConfig, config) if tc.expectedErrMessage != "" { require.Error(t, err) diff --git a/internal/operator-controller/rukpak/render/render.go b/internal/operator-controller/rukpak/render/render.go index a279b5c1e..f7e419c78 100644 --- a/internal/operator-controller/rukpak/render/render.go +++ b/internal/operator-controller/rukpak/render/render.go @@ -124,7 +124,7 @@ func (r BundleRenderer) Render(rv1 bundle.RegistryV1, installNamespace string, o genOpts, errs := (&Options{ // default options InstallNamespace: installNamespace, - TargetNamespaces: defaultTargetNamespacesForBundle(&rv1, installNamespace), + TargetNamespaces: defaultTargetNamespacesForBundle(&rv1), UniqueNameGenerator: DefaultUniqueNameGenerator, CertificateProvider: nil, }).apply(opts...).validate(&rv1) @@ -160,10 +160,10 @@ func validateTargetNamespaces(rv1 *bundle.RegistryV1, installNamespace string, t // in case only the MultiNamespace install mode is supported by the bundle. // If AllNamespaces mode is supported, the default will be [""] -> watch all namespaces // If only OwnNamespace is supported, the default will be [install-namespace] -> only watch the install/own namespace - if supportedInstallModes.Len() == 1 && supportedInstallModes.Has(v1alpha1.InstallModeTypeSingleNamespace) { - return errors.New("exactly one target namespace must be specified") + if supportedInstallModes.Has(v1alpha1.InstallModeTypeMultiNamespace) { + return errors.New("at least one target namespace must be specified") } - return errors.New("at least one target namespace must be specified") + return errors.New("exactly one target namespace must be specified") case set.Len() == 1 && set.Has(""): if supportedInstallModes.Has(v1alpha1.InstallModeTypeAllNamespaces) { return nil @@ -190,17 +190,13 @@ func validateTargetNamespaces(rv1 *bundle.RegistryV1, installNamespace string, t return fmt.Errorf("supported install modes %v do not support target namespaces %v", sets.List[v1alpha1.InstallModeType](supportedInstallModes), targetNamespaces) } -func defaultTargetNamespacesForBundle(rv1 *bundle.RegistryV1, installNamespace string) []string { +func defaultTargetNamespacesForBundle(rv1 *bundle.RegistryV1) []string { supportedInstallModes := supportedBundleInstallModes(rv1) if supportedInstallModes.Has(v1alpha1.InstallModeTypeAllNamespaces) { return []string{corev1.NamespaceAll} } - if supportedInstallModes.Has(v1alpha1.InstallModeTypeOwnNamespace) { - return []string{installNamespace} - } - return nil } diff --git a/internal/operator-controller/rukpak/render/render_test.go b/internal/operator-controller/rukpak/render/render_test.go index 9483bd8cb..ca1459889 100644 --- a/internal/operator-controller/rukpak/render/render_test.go +++ b/internal/operator-controller/rukpak/render/render_test.go @@ -62,6 +62,113 @@ func Test_BundleRenderer_CreatesCorrectDefaultOptions(t *testing.T) { _, _ = renderer.Render(bundle.RegistryV1{}, expectedInstallNamespace) } +func Test_BundleRenderer_DefaultTargetNamespaces(t *testing.T) { + for _, tc := range []struct { + name string + supportedInstallModes []v1alpha1.InstallModeType + expectedTargetNamespaces []string + expectedErrMsg string + }{ + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, OwnNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, SingleNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeMultiNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, OwnNamespace, SingleNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, OwnNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, SingleNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "Default to AllNamespaces when bundle install modes are {AllNamespaces, SingleNamespace, OwnNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedTargetNamespaces: []string{corev1.NamespaceAll}, + }, + { + name: "No default when bundle install modes are {SingleNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + expectedErrMsg: "exactly one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {OwnNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace}, + expectedErrMsg: "exactly one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace}, + expectedErrMsg: "at least one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {SingleNamespace, OwnNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + expectedErrMsg: "exactly one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {SingleNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedErrMsg: "at least one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {OwnNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedErrMsg: "at least one target namespace must be specified", + }, + { + name: "No default when bundle install modes are {SingleNamespace, OwnNamespace, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + expectedErrMsg: "at least one target namespace must be specified", + }, + } { + t.Run(tc.name, func(t *testing.T) { + renderer := render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + require.Equal(t, tc.expectedTargetNamespaces, opts.TargetNamespaces) + return nil, nil + }, + }, + } + _, err := renderer.Render(bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithName("test"). + WithInstallModeSupportFor(tc.supportedInstallModes...).Build(), + }, "some-namespace") + if tc.expectedErrMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMsg) + } else { + require.NoError(t, err) + } + }) + } +} + func Test_BundleRenderer_ValidatesRenderOptions(t *testing.T) { for _, tc := range []struct { name string diff --git a/test/experimental-e2e/single_namespace_support_test.go b/test/experimental-e2e/single_namespace_support_test.go index d1ca13465..77a0cba42 100644 --- a/test/experimental-e2e/single_namespace_support_test.go +++ b/test/experimental-e2e/single_namespace_support_test.go @@ -55,14 +55,14 @@ func TestNoop(t *testing.T) { defer utils.CollectTestArtifacts(t, artifactName, c, cfg) } -func TestClusterExtensionConfigSupport(t *testing.T) { +func TestClusterExtensionSingleNamespaceSupport(t *testing.T) { t.Log("Test support for cluster extension config") defer utils.CollectTestArtifacts(t, artifactName, c, cfg) t.Log("By creating install namespace, watch namespace and necessary rbac resources") namespace := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-operator", + Name: "single-namespace-operator", }, } require.NoError(t, c.Create(t.Context(), &namespace)) @@ -72,7 +72,7 @@ func TestClusterExtensionConfigSupport(t *testing.T) { watchNamespace := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-operator-watch", + Name: "single-namespace-operator-target", }, } require.NoError(t, c.Create(t.Context(), &watchNamespace)) @@ -82,7 +82,7 @@ func TestClusterExtensionConfigSupport(t *testing.T) { serviceAccount := corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-operator-installer", + Name: "single-namespace-operator-installer", Namespace: namespace.GetName(), }, } @@ -93,7 +93,7 @@ func TestClusterExtensionConfigSupport(t *testing.T) { clusterRoleBinding := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-operator-installer", + Name: "single-namespace-operator-installer", }, Subjects: []rbacv1.Subject{ { @@ -114,10 +114,10 @@ func TestClusterExtensionConfigSupport(t *testing.T) { require.NoError(t, c.Delete(context.Background(), clusterRoleBinding)) }) - t.Log("By creating the test-operator ClusterCatalog") + t.Log("By creating the test-catalog ClusterCatalog") extensionCatalog := &ocv1.ClusterCatalog{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-operator-catalog", + Name: "test-catalog", }, Spec: ocv1.ClusterCatalogSpec{ Source: ocv1.CatalogSource{ @@ -143,16 +143,16 @@ func TestClusterExtensionConfigSupport(t *testing.T) { require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) }, pollDuration, pollInterval) - t.Log("By installing the test-operator ClusterExtension configured in SingleNamespace mode") + t.Log("By attempting to install the single-namespace-operator ClusterExtension without any configuration") clusterExtension := &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-operator-extension", + Name: "single-namespace-operator-extension", }, Spec: ocv1.ClusterExtensionSpec{ Source: ocv1.SourceConfig{ SourceType: "Catalog", Catalog: &ocv1.CatalogFilter{ - PackageName: "test", + PackageName: "single-namespace-operator", Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, }, @@ -162,12 +162,153 @@ func TestClusterExtensionConfigSupport(t *testing.T) { ServiceAccount: ocv1.ServiceAccountReference{ Name: serviceAccount.GetName(), }, - Config: &ocv1.ClusterExtensionConfig{ - ConfigType: ocv1.ClusterExtensionConfigTypeInline, - Inline: &apiextensionsv1.JSON{ - Raw: []byte(fmt.Sprintf(`{"watchNamespace": "%s"}`, watchNamespace.GetName())), + }, + } + require.NoError(t, c.Create(t.Context(), clusterExtension)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterExtension)) + }) + + t.Log("By waiting for single-namespace-operator extension installation to fail") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + require.Contains(ct, cond.Message, "required field \"watchNamespace\" is missing") + }, pollDuration, pollInterval) + + t.Log("By updating the ClusterExtension configuration with a watchNamespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(t, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.GetName()}, clusterExtension)) + clusterExtension.Spec.Config = &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(fmt.Sprintf(`{"watchNamespace": "%s"}`, watchNamespace.GetName())), + }, + } + require.NoError(t, c.Update(t.Context(), clusterExtension)) + }, pollDuration, pollInterval) + + t.Log("By waiting for single-namespace-operator extension to be installed successfully") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + require.Contains(ct, cond.Message, "Installed bundle") + require.NotNil(ct, clusterExtension.Status.Install) + require.NotEmpty(ct, clusterExtension.Status.Install.Bundle) + }, pollDuration, pollInterval) + + t.Log("By ensuring the single-namespace-operator deployment is correctly configured to watch the watch namespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + deployment := &appsv1.Deployment{} + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "single-namespace-operator"}, deployment)) + require.NotNil(ct, deployment.Spec.Template.GetAnnotations()) + require.Equal(ct, watchNamespace.GetName(), deployment.Spec.Template.GetAnnotations()["olm.targetNamespaces"]) + }, pollDuration, pollInterval) +} + +func TestClusterExtensionOwnNamespaceSupport(t *testing.T) { + t.Log("Test support for cluster extension with OwnNamespace install mode support") + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) + + t.Log("By creating install namespace, watch namespace and necessary rbac resources") + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "own-namespace-operator", + }, + } + require.NoError(t, c.Create(t.Context(), &namespace)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &namespace)) + }) + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "own-namespace-operator-installer", + Namespace: namespace.GetName(), + }, + } + require.NoError(t, c.Create(t.Context(), &serviceAccount)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), &serviceAccount)) + }) + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "own-namespace-operator-installer", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + APIGroup: corev1.GroupName, + Name: serviceAccount.GetName(), + Namespace: serviceAccount.GetNamespace(), + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }, + } + require.NoError(t, c.Create(t.Context(), clusterRoleBinding)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), clusterRoleBinding)) + }) + + t.Log("By creating the test-catalog ClusterCatalog") + extensionCatalog := &ocv1.ClusterCatalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-catalog", + }, + Spec: ocv1.ClusterCatalogSpec{ + Source: ocv1.CatalogSource{ + Type: ocv1.SourceTypeImage, + Image: &ocv1.ImageSource{ + Ref: fmt.Sprintf("%s/e2e/test-catalog:v1", os.Getenv("CLUSTER_REGISTRY_HOST")), + PollIntervalMinutes: ptr.To(1), + }, + }, + }, + } + require.NoError(t, c.Create(t.Context(), extensionCatalog)) + t.Cleanup(func() { + require.NoError(t, c.Delete(context.Background(), extensionCatalog)) + }) + + t.Log("By waiting for the catalog to serve its metadata") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: extensionCatalog.GetName()}, extensionCatalog)) + cond := apimeta.FindStatusCondition(extensionCatalog.Status.Conditions, ocv1.TypeServing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonAvailable, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("By attempting to install the own-namespace-operator ClusterExtension without any configuration") + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: "own-namespace-operator-extension", + }, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "own-namespace-operator", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/metadata.name": extensionCatalog.Name}, + }, }, }, + Namespace: namespace.GetName(), + ServiceAccount: ocv1.ServiceAccountReference{ + Name: serviceAccount.GetName(), + }, }, } require.NoError(t, c.Create(t.Context(), clusterExtension)) @@ -175,7 +316,51 @@ func TestClusterExtensionConfigSupport(t *testing.T) { require.NoError(t, c.Delete(context.Background(), clusterExtension)) }) - t.Log("By waiting for test-operator extension to be installed successfully") + t.Log("By waiting for own-namespace-operator extension installation to fail") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + require.Contains(ct, cond.Message, "required field \"watchNamespace\" is missing") + }, pollDuration, pollInterval) + + t.Log("By updating the ClusterExtension configuration with a watchNamespace other than the install namespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(t, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.GetName()}, clusterExtension)) + clusterExtension.Spec.Config = &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "some-namespace"}`), + }, + } + require.NoError(t, c.Update(t.Context(), clusterExtension)) + }, pollDuration, pollInterval) + + t.Log("By waiting for own-namespace-operator extension installation to fail") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonRetrying, cond.Reason) + require.Contains(ct, cond.Message, fmt.Sprintf("invalid 'watchNamespace' \"some-namespace\": must be install namespace (%s)", clusterExtension.Spec.Namespace)) + }, pollDuration, pollInterval) + + t.Log("By updating the ClusterExtension configuration with a watchNamespace = install namespace") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(t, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.GetName()}, clusterExtension)) + clusterExtension.Spec.Config = &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(fmt.Sprintf(`{"watchNamespace": "%s"}`, clusterExtension.Spec.Namespace)), + }, + } + require.NoError(t, c.Update(t.Context(), clusterExtension)) + }, pollDuration, pollInterval) + + t.Log("By waiting for own-namespace-operator extension to be installed successfully") require.EventuallyWithT(t, func(ct *assert.CollectT) { require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled) @@ -187,12 +372,12 @@ func TestClusterExtensionConfigSupport(t *testing.T) { require.NotEmpty(ct, clusterExtension.Status.Install.Bundle) }, pollDuration, pollInterval) - t.Log("By ensuring the test-operator deployment is correctly configured to watch the watch namespace") + t.Log("By ensuring the own-namespace-operator deployment is correctly configured to watch the watch namespace") require.EventuallyWithT(t, func(ct *assert.CollectT) { deployment := &appsv1.Deployment{} - require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "test-operator"}, deployment)) + require.NoError(ct, c.Get(t.Context(), types.NamespacedName{Namespace: namespace.GetName(), Name: "own-namespace-operator"}, deployment)) require.NotNil(ct, deployment.Spec.Template.GetAnnotations()) - require.Equal(ct, watchNamespace.GetName(), deployment.Spec.Template.GetAnnotations()["olm.targetNamespaces"]) + require.Equal(ct, clusterExtension.Spec.Namespace, deployment.Spec.Template.GetAnnotations()["olm.targetNamespaces"]) }, pollDuration, pollInterval) } diff --git a/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_ownnamespaces.yaml b/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_ownnamespaces.yaml new file mode 100644 index 000000000..305e3c73e --- /dev/null +++ b/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_ownnamespaces.yaml @@ -0,0 +1,27 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: ownnamespaces.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: OwnNamespace + listKind: OwnNamespaceList + plural: ownnamespaces + singular: ownnamespace + scope: Cluster + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + testField: + type: string diff --git a/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/ownnamespaceoperator.clusterserviceversion.yaml b/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/ownnamespaceoperator.clusterserviceversion.yaml new file mode 100644 index 000000000..d6c2a3255 --- /dev/null +++ b/testdata/images/bundles/own-namespace-operator/v1.0.0/manifests/ownnamespaceoperator.clusterserviceversion.yaml @@ -0,0 +1,151 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "ownnamespaces.olm.operatorframework.io/v1", + "kind": "OwnNamespace", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "test" + }, + "name": "test-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2024-10-24T19:21:40Z" + operators.operatorframework.io/builder: operator-sdk-v1.34.1 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: ownnamespaceoperator.v1.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: A dummy resource for an operator that only supports own namespace install mode + displayName: OwnNamespace + kind: OwnNamespace + name: ownnamespaces.olm.operatorframework.io + version: v1 + description: OLM OwnNamespace Testing Operator + displayName: test-operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: + - label: + app.kubernetes.io/component: controller + app.kubernetes.io/name: own-namespace-operator + app.kubernetes.io/version: 1.0.0 + name: own-namespace-operator + spec: + replicas: 1 + selector: + matchLabels: + app: ownnamespacetest + template: + metadata: + labels: + app: ownnamespacetest + spec: + terminationGracePeriodSeconds: 0 + containers: + - name: busybox + image: busybox:1.36 + command: + - 'sleep' + - '1000' + securityContext: + runAsUser: 1000 + runAsNonRoot: true + serviceAccountName: simple-bundle-manager + clusterPermissions: + - rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: simple-bundle-manager + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - create + - update + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: simple-bundle-manager + strategy: deployment + installModes: + - supported: true + type: OwnNamespace + - supported: false + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: false + type: AllNamespaces + keywords: + - registry + links: + - name: simple-bundle + url: https://simple-bundle.domain + maintainers: + - email: main#simple-bundle.domain + name: Simple Bundle + maturity: beta + provider: + name: Simple Bundle + url: https://simple-bundle.domain + version: 1.0.0 diff --git a/testdata/images/bundles/own-namespace-operator/v1.0.0/metadata/annotations.yaml b/testdata/images/bundles/own-namespace-operator/v1.0.0/metadata/annotations.yaml new file mode 100644 index 000000000..24cf5213a --- /dev/null +++ b/testdata/images/bundles/own-namespace-operator/v1.0.0/metadata/annotations.yaml @@ -0,0 +1,10 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: own-namespace-operator + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: unknown diff --git a/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_singlenamespaces.yaml b/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_singlenamespaces.yaml new file mode 100644 index 000000000..5f8d30543 --- /dev/null +++ b/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/olm.operatorframework.com_singlenamespaces.yaml @@ -0,0 +1,27 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: singlenamespaces.olm.operatorframework.io +spec: + group: olm.operatorframework.io + names: + kind: SingleNamespace + listKind: SingleNamespaceList + plural: singlenamespaces + singular: singlenamespace + scope: Cluster + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + testField: + type: string diff --git a/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/singlenamespaceoperator.clusterserviceversion.yaml b/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/singlenamespaceoperator.clusterserviceversion.yaml new file mode 100644 index 000000000..dea4c7f09 --- /dev/null +++ b/testdata/images/bundles/single-namespace-operator/v1.0.0/manifests/singlenamespaceoperator.clusterserviceversion.yaml @@ -0,0 +1,151 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [ + { + "apiVersion": "singlenamespaces.olm.operatorframework.io/v1", + "kind": "SingleNamespace", + "metadata": { + "labels": { + "app.kubernetes.io/managed-by": "kustomize", + "app.kubernetes.io/name": "test" + }, + "name": "test-sample" + }, + "spec": null + } + ] + capabilities: Basic Install + createdAt: "2024-10-24T19:21:40Z" + operators.operatorframework.io/builder: operator-sdk-v1.34.1 + operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 + name: singlenamespaceoperator.v1.0.0 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: A dummy resource for an operator that only supports single namespace install mode + displayName: SingleNamespace + kind: SingleNamespace + name: singlenamespaces.olm.operatorframework.io + version: v1 + description: OLM SingleNamespace Testing Operator + displayName: test-operator + icon: + - base64data: "" + mediatype: "" + install: + spec: + deployments: + - label: + app.kubernetes.io/component: controller + app.kubernetes.io/name: single-namespace-operator + app.kubernetes.io/version: 1.0.0 + name: single-namespace-operator + spec: + replicas: 1 + selector: + matchLabels: + app: singlenamespacetest + template: + metadata: + labels: + app: singlenamespacetest + spec: + terminationGracePeriodSeconds: 0 + containers: + - name: busybox + image: busybox:1.36 + command: + - 'sleep' + - '1000' + securityContext: + runAsUser: 1000 + runAsNonRoot: true + serviceAccountName: simple-bundle-manager + clusterPermissions: + - rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create + serviceAccountName: simple-bundle-manager + permissions: + - rules: + - apiGroups: + - "" + resources: + - configmaps + - serviceaccounts + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - get + - list + - create + - update + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + serviceAccountName: simple-bundle-manager + strategy: deployment + installModes: + - supported: false + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: false + type: AllNamespaces + keywords: + - registry + links: + - name: simple-bundle + url: https://simple-bundle.domain + maintainers: + - email: main#simple-bundle.domain + name: Simple Bundle + maturity: beta + provider: + name: Simple Bundle + url: https://simple-bundle.domain + version: 1.0.0 diff --git a/testdata/images/bundles/single-namespace-operator/v1.0.0/metadata/annotations.yaml b/testdata/images/bundles/single-namespace-operator/v1.0.0/metadata/annotations.yaml new file mode 100644 index 000000000..b7d60e6df --- /dev/null +++ b/testdata/images/bundles/single-namespace-operator/v1.0.0/metadata/annotations.yaml @@ -0,0 +1,10 @@ +annotations: + # Core bundle annotations. + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: single-namespace-operator + operators.operatorframework.io.bundle.channels.v1: alpha + operators.operatorframework.io.metrics.builder: operator-sdk-v1.28.0 + operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 + operators.operatorframework.io.metrics.project_layout: unknown diff --git a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml index 65f799a0a..437175a4e 100644 --- a/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml +++ b/testdata/images/catalogs/test-catalog/v1/configs/catalog.yaml @@ -107,3 +107,43 @@ properties: value: packageName: webhook-operator version: 0.0.1 +--- +schema: olm.package +name: own-namespace-operator +defaultChannel: alpha +--- +schema: olm.channel +name: alpha +package: own-namespace-operator +entries: + - name: own-namespace-operator.1.0.0 +--- +schema: olm.bundle +name: own-namespace-operator.1.0.0 +package: own-namespace-operator +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/own-namespace-operator:v1.0.0 +properties: + - type: olm.package + value: + packageName: own-namespace-operator + version: 1.0.0 +--- +schema: olm.package +name: single-namespace-operator +defaultChannel: alpha +--- +schema: olm.channel +name: alpha +package: single-namespace-operator +entries: + - name: single-namespace-operator.1.0.0 +--- +schema: olm.bundle +name: single-namespace-operator.1.0.0 +package: single-namespace-operator +image: docker-registry.operator-controller-e2e.svc.cluster.local:5000/bundles/registry-v1/single-namespace-operator:v1.0.0 +properties: + - type: olm.package + value: + packageName: single-namespace-operator + version: 1.0.0