diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 85f2e823d..b408d21a4 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -655,9 +655,10 @@ func setupHelm( ActionClientGetter: acg, Preflights: preflights, HelmChartProvider: &applier.RegistryV1HelmChartProvider{ - BundleRenderer: registryv1.Renderer, - CertificateProvider: certProvider, - IsWebhookSupportEnabled: certProvider != nil, + BundleRenderer: registryv1.Renderer, + CertificateProvider: certProvider, + IsWebhookSupportEnabled: certProvider != nil, + IsSingleOwnNamespaceEnabled: features.OperatorControllerFeatureGate.Enabled(features.SingleOwnNamespaceInstallSupport), }, HelmReleaseToObjectsConverter: &applier.HelmReleaseToObjectsConverter{}, PreAuthorizer: preAuth, diff --git a/internal/operator-controller/applier/provider.go b/internal/operator-controller/applier/provider.go index e0b715360..8a7123243 100644 --- a/internal/operator-controller/applier/provider.go +++ b/internal/operator-controller/applier/provider.go @@ -6,6 +6,9 @@ import ( "fmt" "helm.sh/helm/v3/pkg/chart" + "k8s.io/apimachinery/pkg/util/sets" + + "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/source" @@ -13,9 +16,10 @@ import ( ) type RegistryV1HelmChartProvider struct { - BundleRenderer render.BundleRenderer - CertificateProvider render.CertificateProvider - IsWebhookSupportEnabled bool + BundleRenderer render.BundleRenderer + CertificateProvider render.CertificateProvider + IsWebhookSupportEnabled bool + IsSingleOwnNamespaceEnabled bool } func (r *RegistryV1HelmChartProvider) Get(bundle source.BundleSource, ext *ocv1.ClusterExtension) (*chart.Chart, error) { @@ -24,18 +28,6 @@ func (r *RegistryV1HelmChartProvider) Get(bundle source.BundleSource, ext *ocv1. return nil, err } - watchNamespace, err := GetWatchNamespace(ext) - if err != nil { - return nil, err - } - - opts := []render.Option{ - render.WithCertificateProvider(r.CertificateProvider), - } - if watchNamespace != "" { - opts = append(opts, render.WithTargetNamespaces(watchNamespace)) - } - if len(rv1.CSV.Spec.APIServiceDefinitions.Owned) > 0 { return nil, fmt.Errorf("unsupported bundle: apiServiceDefintions are not supported") } @@ -48,8 +40,35 @@ func (r *RegistryV1HelmChartProvider) Get(bundle source.BundleSource, ext *ocv1. } } - if r.CertificateProvider == nil && len(rv1.CSV.Spec.WebhookDefinitions) > 0 { - return nil, fmt.Errorf("unsupported bundle: webhookDefinitions are not supported") + installModes := sets.New(rv1.CSV.Spec.InstallModes...) + if !r.IsSingleOwnNamespaceEnabled && !installModes.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}) { + return nil, fmt.Errorf("unsupported bundle: bundle does not support AllNamespaces install mode") + } + + if !installModes.HasAny( + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}, + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}, + v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}, + ) { + return nil, fmt.Errorf("unsupported bundle: bundle must support at least one of [AllNamespaces SingleNamespace OwnNamespace] install modes") + } + + opts := []render.Option{ + render.WithCertificateProvider(r.CertificateProvider), + } + + // TODO: in a follow up PR we'll split this into two components: + // 1. takes a bundle + cluster extension => manifests + // 2. takes a bundle + cluster extension => chart (which will use the component in 1. under the hood) + // GetWatchNamespace will move under the component in 1. and also be reused by the component that + // takes bundle + cluster extension => revision + watchNamespace, err := GetWatchNamespace(ext) + if err != nil { + return nil, err + } + + if watchNamespace != "" { + opts = append(opts, render.WithTargetNamespaces(watchNamespace)) } objs, err := r.BundleRenderer.Render(rv1, ext.Spec.Namespace, opts...) diff --git a/internal/operator-controller/applier/provider_test.go b/internal/operator-controller/applier/provider_test.go index 3e2003b63..b1345dd30 100644 --- a/internal/operator-controller/applier/provider_test.go +++ b/internal/operator-controller/applier/provider_test.go @@ -85,6 +85,78 @@ func Test_RegistryV1HelmChartProvider_Get_NoAPIServiceDefinitions(t *testing.T) require.Contains(t, err.Error(), "unsupported bundle: apiServiceDefintions are not supported") } +func Test_RegistryV1HelmChartProvider_Get_SingleOwnNamespace(t *testing.T) { + t.Run("rejects bundles without AllNamespaces install mode support if SingleOwnNamespace is not enabled", func(t *testing.T) { + provider := applier.RegistryV1HelmChartProvider{} + + b := source.FromBundle( + bundle.RegistryV1{ + CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace)), + }, + ) + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + + _, err := provider.Get(b, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported bundle: bundle does not support AllNamespaces install mode") + }) + t.Run("accepts bundles with SingleNamespace install mode support if SingleOwnNamespace is enabled", func(t *testing.T) { + // TODO: this will be removed in a follow-up PR that will refactor GetWatchNamespace's location + featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.SingleOwnNamespaceInstallSupport, true) + provider := applier.RegistryV1HelmChartProvider{ + IsSingleOwnNamespaceEnabled: true, + } + + b := source.FromBundle( + bundle.RegistryV1{ + CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace)), + }, + ) + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + Config: &ocv1.ClusterExtensionConfig{ + ConfigType: ocv1.ClusterExtensionConfigTypeInline, + Inline: &apiextensionsv1.JSON{ + Raw: []byte(`{"watchNamespace": "some-namespace"}`), + }, + }, + }, + } + + _, err := provider.Get(b, ext) + require.NoError(t, err) + }) + t.Run("accepts bundles with OwnNamespace install mode support if SingleOwnNamespace is enabled", func(t *testing.T) { + // TODO: this will be removed in a follow-up PR that will refactor GetWatchNamespace's location + featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.SingleOwnNamespaceInstallSupport, true) + provider := applier.RegistryV1HelmChartProvider{ + IsSingleOwnNamespaceEnabled: true, + } + + b := source.FromBundle( + bundle.RegistryV1{ + CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeOwnNamespace)), + }, + ) + + ext := &ocv1.ClusterExtension{ + Spec: ocv1.ClusterExtensionSpec{ + Namespace: "install-namespace", + }, + } + + _, err := provider.Get(b, ext) + require.NoError(t, err) + }) +} + func Test_RegistryV1HelmChartProvider_Get_NoWebhooksWithoutCertProvider(t *testing.T) { provider := applier.RegistryV1HelmChartProvider{ IsWebhookSupportEnabled: true,