diff --git a/internal/operator-controller/applier/provider.go b/internal/operator-controller/applier/provider.go index 4e957aadc..f425e7415 100644 --- a/internal/operator-controller/applier/provider.go +++ b/internal/operator-controller/applier/provider.go @@ -3,20 +3,17 @@ package applier import ( "crypto/sha256" "encoding/json" - "errors" "fmt" "io/fs" - "strings" "helm.sh/helm/v3/pkg/chart" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" "github.com/operator-framework/api/pkg/operators/v1alpha1" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" ) @@ -35,9 +32,6 @@ type RegistryV1ManifestProvider struct { IsWebhookSupportEnabled bool IsSingleOwnNamespaceEnabled bool } -type registryV1Config struct { - WatchNamespace string `json:"watchNamespace"` -} func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) { rv1, err := source.FromFS(bundleFS).GetBundle() @@ -74,44 +68,18 @@ func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtens render.WithCertificateProvider(r.CertificateProvider), } - watchNamespace, err := r.getWatchNamespace(ext) - if err != nil { - return nil, fmt.Errorf("invalid bundle configuration: %w", err) - } - - if watchNamespace != "" { - opts = append(opts, render.WithTargetNamespaces(watchNamespace)) - } - - return r.BundleRenderer.Render(rv1, ext.Spec.Namespace, opts...) -} - -// getWatchNamespace determines the watch namespace the ClusterExtension should use based on the -// configuration in .spec.config.Inline. Only active if SingleOwnNamespace support is enabled. -func (r *RegistryV1ManifestProvider) getWatchNamespace(ext *ocv1.ClusterExtension) (string, error) { - if !r.IsSingleOwnNamespaceEnabled { - return "", nil - } - - var watchNamespace string - if ext.Spec.Config != nil && ext.Spec.Config.Inline != nil { - cfg := ®istryV1Config{} - // Using k8s.io/yaml package as that is able to handle both json and yaml - // In most cases, at this point we should have a valid JSON/YAML object in the byte slice and failures will - // be related to object structure (e.g. additional fields). - if err := yaml.UnmarshalStrict(ext.Spec.Config.Inline.Raw, cfg); err != nil { - return "", fmt.Errorf("error unmarshalling registry+v1 configuration: %w", formatUnmarshallError(err)) + 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 err != nil { + return nil, fmt.Errorf("invalid bundle configuration: %w", err) } - watchNamespace = cfg.WatchNamespace - } else { - return "", nil - } - if errs := validation.IsDNS1123Subdomain(watchNamespace); len(errs) > 0 { - return "", fmt.Errorf("invalid watch namespace '%s': namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", watchNamespace) + if bundleConfig != nil && bundleConfig.WatchNamespace != nil { + opts = append(opts, render.WithTargetNamespaces(*bundleConfig.WatchNamespace)) + } } - return watchNamespace, nil + return r.BundleRenderer.Render(rv1, ext.Spec.Namespace, opts...) } // RegistryV1HelmChartProvider creates a Helm-Chart from a registry+v1 bundle and its associated ClusterExtension @@ -160,27 +128,3 @@ func (r *RegistryV1HelmChartProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExten return chrt, nil } - -func formatUnmarshallError(err error) error { - var unmarshalErr *json.UnmarshalTypeError - if errors.As(err, &unmarshalErr) { - if unmarshalErr.Field == "" { - return errors.New("input is not a valid JSON object") - } else { - return fmt.Errorf("invalid value type for field %q: expected %q but got %q", unmarshalErr.Field, unmarshalErr.Type.String(), unmarshalErr.Value) - } - } - - // unwrap error until the core and process it - for { - unwrapped := errors.Unwrap(err) - if unwrapped == nil { - // usually the errors present in the form json: or yaml: - // we want to extract if we can - errMessageComponents := strings.Split(err.Error(), ":") - coreErrMessage := strings.TrimSpace(errMessageComponents[len(errMessageComponents)-1]) - return errors.New(coreErrMessage) - } - err = unwrapped - } -} diff --git a/internal/operator-controller/applier/provider_test.go b/internal/operator-controller/applier/provider_test.go index a34b0abb2..b1a6cd4f4 100644 --- a/internal/operator-controller/applier/provider_test.go +++ b/internal/operator-controller/applier/provider_test.go @@ -64,6 +64,42 @@ func Test_RegistryV1ManifestProvider_Integration(t *testing.T) { require.Contains(t, err.Error(), "some error") }) + t.Run("surfaces bundle config unmarshall errors", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: render.BundleRenderer{ + ResourceGenerators: []render.ResourceGenerator{ + func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { + return nil, nil + }, + }, + }, + // must be true for now as we only unmarshal configuration when this feature is on + // once we go GA and remove IsSingleOwnNamespaceEnabled it's ok to just delete this + IsSingleOwnNamespaceEnabled: true, + } + + // The contents of the bundle are not important for this tesy, only that it be a valid bundle + // to avoid errors in the deserialization process + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "install-namespace"}`), + }, + }, + }, + } + + _, err := provider.Get(bundleFS, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid bundle configuration") + }) + t.Run("returns rendered manifests", func(t *testing.T) { provider := applier.RegistryV1ManifestProvider{ BundleRenderer: registryv1.Renderer, @@ -188,77 +224,6 @@ func Test_RegistryV1ManifestProvider_WebhookSupport(t *testing.T) { }) } -func Test_RegistryV1ManifestProvider_ConfigUnmarshalling(t *testing.T) { - for _, tc := range []struct { - name string - configBytes []byte - expectedErrMessage string - }{ - { - name: "accepts json config", - configBytes: []byte(`{"watchNamespace": "some-namespace"}`), - }, - { - name: "accepts yaml config", - configBytes: []byte(`watchNamespace: some-namespace`), - }, - { - name: "rejects invalid json", - configBytes: []byte(`{"hello`), - expectedErrMessage: `invalid bundle configuration: error unmarshalling registry+v1 configuration: found unexpected end of stream`, - }, - { - name: "rejects valid json that isn't of object type", - configBytes: []byte(`true`), - expectedErrMessage: `invalid bundle configuration: error unmarshalling registry+v1 configuration: input is not a valid JSON object`, - }, - { - name: "rejects additional fields", - configBytes: []byte(`somekey: somevalue`), - expectedErrMessage: `invalid bundle configuration: error unmarshalling registry+v1 configuration: unknown field "somekey"`, - }, - { - name: "rejects valid json but invalid registry+v1", - configBytes: []byte(`{"watchNamespace": {"hello": "there"}}`), - expectedErrMessage: `invalid bundle configuration: error unmarshalling registry+v1 configuration: invalid value type for field "watchNamespace": expected "string" but got "object"`, - }, - } { - t.Run(tc.name, func(t *testing.T) { - provider := applier.RegistryV1ManifestProvider{ - BundleRenderer: render.BundleRenderer{ - ResourceGenerators: []render.ResourceGenerator{ - func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) { - return nil, nil - }, - }, - }, - IsSingleOwnNamespaceEnabled: true, - } - - bundleFS := bundlefs.Builder().WithPackageName("test"). - WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build() - - _, err := provider.Get(bundleFS, &ocv1.ClusterExtension{ - Spec: ocv1.ClusterExtensionSpec{ - Namespace: "install-namespace", - Config: &ocv1.ClusterExtensionConfig{ - ConfigType: ocv1.ClusterExtensionConfigTypeInline, - Inline: &apiextensionsv1.JSON{ - Raw: tc.configBytes, - }, - }, - }, - }) - if tc.expectedErrMessage != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tc.expectedErrMessage) - } else { - require.NoError(t, err) - } - }) - } -} - func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) { t.Run("rejects bundles without AllNamespaces install mode when Single/OwnNamespace install mode support is disabled", func(t *testing.T) { provider := applier.RegistryV1ManifestProvider{ @@ -276,6 +241,54 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) { require.Equal(t, "unsupported bundle: bundle does not support AllNamespaces install mode", err.Error()) }) + 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, + } + + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).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": "` + expectedWatchNamespace + `"}`), + }, + }, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported bundle") + }) + + t.Run("rejects bundles without AllNamespaces install mode and with OwnNamespace support when Single/OwnNamespace install mode support is disabled", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + IsSingleOwnNamespaceEnabled: false, + } + 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(), "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) { expectedWatchNamespace := "some-namespace" provider := applier.RegistryV1ManifestProvider{ diff --git a/internal/operator-controller/rukpak/bundle/config.go b/internal/operator-controller/rukpak/bundle/config.go new file mode 100644 index 000000000..c65a2311a --- /dev/null +++ b/internal/operator-controller/rukpak/bundle/config.go @@ -0,0 +1,125 @@ +package bundle + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/yaml" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +type Config struct { + WatchNamespace *string `json:"watchNamespace"` +} + +// UnmarshallConfig returns a deserialized and validated *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 == 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)) + } + + // collect bundle install modes + bundleInstallModeSet := sets.New(rv1.CSV.Spec.InstallModes...) + + if err := validateConfig(bundleConfig, installNamespace, bundleInstallModeSet); err != nil { + return nil, fmt.Errorf("error unmarshalling registry+v1 configuration: %w", err) + } + + return bundleConfig, nil +} + +// validateConfig validates a *bundle.Config against the bundle's supported install modes and the user-give installNamespace. +func validateConfig(config *Config, installNamespace string, bundleInstallModeSet sets.Set[v1alpha1.InstallMode]) error { + // no config, no problem + if config == nil { + return nil + } + + // if the bundle does not support the watchNamespace configuration and it is set, treat it like any unknown field + if config.WatchNamespace != nil && !isWatchNamespaceConfigSupported(bundleInstallModeSet) { + return errors.New(`unknown field "watchNamespace"`) + } + + // if watchNamespace is required then ensure that it is set + if config.WatchNamespace == nil && isWatchNamespaceConfigRequired(bundleInstallModeSet) { + return errors.New(`required field "watchNamespace" is missing`) + } + + // if watchNamespace is set then ensure it is a valid namespace + if config.WatchNamespace != nil { + if errs := validation.IsDNS1123Subdomain(*config.WatchNamespace); len(errs) > 0 { + return fmt.Errorf("invalid 'watchNamespace' %q: namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", *config.WatchNamespace) + } + } + + // only accept install namespace if OwnNamespace install mode is supported + if config.WatchNamespace != nil && *config.WatchNamespace == installNamespace && + !bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}) { + return fmt.Errorf("invalid 'watchNamespace' %q: must not be install namespace (%s)", *config.WatchNamespace, installNamespace) + } + + // only accept non-install namespace is SingleNamespace is supported + if config.WatchNamespace != nil && *config.WatchNamespace != installNamespace && + !bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}) { + return fmt.Errorf("invalid 'watchNamespace' %q: must be install namespace (%s)", *config.WatchNamespace, installNamespace) + } + + return nil +} + +// 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 +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}) +} + +// isWatchNamespaceConfigRequired returns true if the watchNamespace configuration is required. This happens when +// AllNamespaces install mode is not supported and SingleNamespace 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}) +} + +// formatUnmarshallError format JSON unmarshal errors to be more readable +func formatUnmarshallError(err error) error { + var unmarshalErr *json.UnmarshalTypeError + if errors.As(err, &unmarshalErr) { + if unmarshalErr.Field == "" { + return errors.New("input is not a valid JSON object") + } else { + return fmt.Errorf("invalid value type for field %q: expected %q but got %q", unmarshalErr.Field, unmarshalErr.Type.String(), unmarshalErr.Value) + } + } + + // unwrap error until the core and process it + for { + unwrapped := errors.Unwrap(err) + if unwrapped == nil { + // usually the errors present in the form json: or yaml: + // we want to extract if we can + errMessageComponents := strings.Split(err.Error(), ":") + coreErrMessage := strings.TrimSpace(errMessageComponents[len(errMessageComponents)-1]) + return errors.New(coreErrMessage) + } + err = unwrapped + } +} diff --git a/internal/operator-controller/rukpak/bundle/config_test.go b/internal/operator-controller/rukpak/bundle/config_test.go new file mode 100644 index 000000000..10494f651 --- /dev/null +++ b/internal/operator-controller/rukpak/bundle/config_test.go @@ -0,0 +1,259 @@ +package bundle_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion" +) + +func Test_UnmarshallConfig(t *testing.T) { + for _, tc := range []struct { + name string + rawConfig []byte + supportedInstallModes []v1alpha1.InstallModeType + installNamespace string + expectedErrMessage string + expectedConfig *bundle.Config + }{ + { + name: "accepts nil raw config", + rawConfig: nil, + expectedConfig: nil, + }, + { + name: "accepts json config", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts yaml config", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`watchNamespace: some-namespace`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "rejects invalid json", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"hello`), + expectedErrMessage: `error unmarshalling registry+v1 configuration: found unexpected end of stream`, + }, + { + name: "rejects valid json that isn't of object type", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`true`), + expectedErrMessage: `error unmarshalling registry+v1 configuration: input is not a valid JSON object`, + }, + { + name: "rejects additional fields", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`somekey: somevalue`), + expectedErrMessage: `error unmarshalling registry+v1 configuration: unknown field "somekey"`, + }, + { + name: "rejects valid json but invalid registry+v1", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": {"hello": "there"}}`), + expectedErrMessage: `error unmarshalling registry+v1 configuration: invalid value type for field "watchNamespace": expected "string" but got "object"`, + }, + { + name: "rejects bad namespace format", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "bad-Namespace-"}`), + expectedErrMessage: "invalid 'watchNamespace' \"bad-Namespace-\": namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", + }, + { + name: "rejects with unknown field when install modes {AllNamespaces}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedErrMessage: "unknown field \"watchNamespace\"", + }, + { + name: "rejects with unknown field when install modes {MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedErrMessage: "unknown field \"watchNamespace\"", + }, + { + name: "reject with unknown field when install modes {AllNamespaces, MultiNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedErrMessage: "unknown field \"watchNamespace\"", + }, + { + name: "reject with unknown field when install modes {OwnNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedErrMessage: "unknown field \"watchNamespace\"", + }, + { + name: "reject with unknown field when install modes {MultiNamespace, OwnNamespace}", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedErrMessage: "unknown field \"watchNamespace\"", + }, + { + name: "accepts when install modes {SingleNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts when install modes {AllNamespaces, SingleNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts when install modes {MultiNamespace, SingleNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts when install modes {OwnNamespace, SingleNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "not-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "rejects when install modes {SingleNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)", + }, + { + name: "rejects when install modes {AllNamespaces, SingleNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)", + }, + { + name: "rejects when install modes {MultiNamespace, SingleNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)", + }, + { + name: "accepts when install modes {AllNamespaces, OwnNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "accepts when install modes {OwnNamespace, SingleNamespace} and watchNamespace == install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: ptr.To("some-namespace"), + }, + }, + { + name: "rejects when install modes {AllNamespaces, OwnNamespace} and watchNamespace != install namespace", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": "some-namespace"}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must be install namespace (not-some-namespace)", + }, + { + name: "rejects with required field error when install modes {SingleNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, OwnNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "rejects with required field error when install modes {SingleNamespace, MultiNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + 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}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "required field \"watchNamespace\" is missing", + }, + { + name: "accepts null watchNamespace when install modes {AllNamespaces, OwnNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + installNamespace: "not-some-namespace", + expectedConfig: &bundle.Config{ + WatchNamespace: nil, + }, + }, + { + name: "accepts null watchNamespace when install modes {AllNamespaces, OwnNamespace, MultiNamespace} and watchNamespace is nil", + supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeMultiNamespace}, + rawConfig: []byte(`{"watchNamespace": null}`), + 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}, + rawConfig: []byte(`{"watchNamespace": ""}`), + installNamespace: "not-some-namespace", + expectedErrMessage: "invalid 'watchNamespace' \"\": namespace must consist of lower case alphanumeric characters", + }, + } { + t.Run(tc.name, func(t *testing.T) { + var rv1 bundle.RegistryV1 + if tc.supportedInstallModes != nil { + rv1 = bundle.RegistryV1{ + CSV: clusterserviceversion.Builder(). + WithName("test-operator"). + WithInstallModeSupportFor(tc.supportedInstallModes...). + Build(), + } + } + + config, err := bundle.UnmarshallConfig(tc.rawConfig, rv1, tc.installNamespace) + require.Equal(t, tc.expectedConfig, config) + if tc.expectedErrMessage != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMessage) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/operator-controller/rukpak/render/fake.go b/internal/operator-controller/rukpak/render/fake.go new file mode 100644 index 000000000..c8213d78a --- /dev/null +++ b/internal/operator-controller/rukpak/render/fake.go @@ -0,0 +1,24 @@ +package render + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type FakeCertProvider struct { + InjectCABundleFn func(obj client.Object, cfg CertificateProvisionerConfig) error + AdditionalObjectsFn func(cfg CertificateProvisionerConfig) ([]unstructured.Unstructured, error) + GetCertSecretInfoFn func(cfg CertificateProvisionerConfig) CertSecretInfo +} + +func (f FakeCertProvider) InjectCABundle(obj client.Object, cfg CertificateProvisionerConfig) error { + return f.InjectCABundleFn(obj, cfg) +} + +func (f FakeCertProvider) AdditionalObjects(cfg CertificateProvisionerConfig) ([]unstructured.Unstructured, error) { + return f.AdditionalObjectsFn(cfg) +} + +func (f FakeCertProvider) GetCertSecretInfo(cfg CertificateProvisionerConfig) CertSecretInfo { + return f.GetCertSecretInfoFn(cfg) +}