diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 2fcea83ef0..b6ea5c6823 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -502,11 +502,12 @@ func run() error { certProvider := getCertificateProvider() regv1ManifestProvider := &applier.RegistryV1ManifestProvider{ - BundleRenderer: registryv1.Renderer, - CertificateProvider: certProvider, - IsWebhookSupportEnabled: certProvider != nil, - IsSingleOwnNamespaceEnabled: features.OperatorControllerFeatureGate.Enabled(features.SingleOwnNamespaceInstallSupport), - IsDeploymentConfigEnabled: features.OperatorControllerFeatureGate.Enabled(features.DeploymentConfig), + BundleRenderer: registryv1.Renderer, + CertificateProvider: certProvider, + IsWebhookSupportEnabled: certProvider != nil, + IsSingleOwnNamespaceEnabled: features.OperatorControllerFeatureGate.Enabled(features.SingleOwnNamespaceInstallSupport), + IsDeploymentConfigEnabled: features.OperatorControllerFeatureGate.Enabled(features.DeploymentConfig), + IsNamespaceManagementEnabled: features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime), } var cerCfg reconcilerConfigurator if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) { @@ -659,6 +660,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl controllers.RetrieveRevisionStates(revisionStatesGetter), controllers.ResolveBundle(c.resolver, c.mgr.GetClient()), controllers.UnpackBundle(c.imagePuller, c.imageCache), + controllers.ValidateInstallNamespace(coreClient), controllers.ApplyBundleWithBoxcutter(appl.Apply), } diff --git a/internal/operator-controller/applier/boxcutter.go b/internal/operator-controller/applier/boxcutter.go index a52fa21c7e..5914e864c4 100644 --- a/internal/operator-controller/applier/boxcutter.go +++ b/internal/operator-controller/applier/boxcutter.go @@ -273,6 +273,11 @@ type boxcutterStorageMigratorClient interface { // Migrate creates a ClusterObjectSet from an existing Helm release if no revisions exist yet. // The migration is idempotent and skipped if revisions already exist or no Helm release is found. func (m *BoxcutterStorageMigrator) Migrate(ctx context.Context, ext *ocv1.ClusterExtension, objectLabels map[string]string) error { + // Managed namespace mode (spec.namespace empty) means this is a new-style extension + // that never had a Helm release, so there's nothing to migrate. + if ext.Spec.Namespace == "" { + return nil + } existingRevisionList := ocv1.ClusterObjectSetList{} if err := m.Client.List(ctx, &existingRevisionList, client.MatchingLabels{ labels.OwnerNameKey: ext.Name, diff --git a/internal/operator-controller/applier/boxcutter_test.go b/internal/operator-controller/applier/boxcutter_test.go index 25963c9a01..d33c6aa55e 100644 --- a/internal/operator-controller/applier/boxcutter_test.go +++ b/internal/operator-controller/applier/boxcutter_test.go @@ -1141,7 +1141,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, ocv1.AddToScheme(testScheme)) ext := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, Spec: ocv1.ClusterExtensionSpec{Namespace: "test-namespace"}, } ctrl := gomock.NewController(t) brb := newStorageMigratorGenerator(t) @@ -1214,7 +1214,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, ocv1.AddToScheme(testScheme)) ext := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, Spec: ocv1.ClusterExtensionSpec{Namespace: "test-namespace"}, } // GenerateRevisionFromHelmRelease should not be called when revisions already exist ctrl := gomock.NewController(t) @@ -1269,7 +1269,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, ocv1.AddToScheme(testScheme)) ext := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, Spec: ocv1.ClusterExtensionSpec{Namespace: "test-namespace"}, } ctrl := gomock.NewController(t) brb := mockapplier.NewMockClusterObjectSetGenerator(ctrl) @@ -1342,7 +1342,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, ocv1.AddToScheme(testScheme)) ext := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, Spec: ocv1.ClusterExtensionSpec{Namespace: "test-namespace"}, } ctrl := gomock.NewController(t) brb := mockapplier.NewMockClusterObjectSetGenerator(ctrl) @@ -1425,7 +1425,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, ocv1.AddToScheme(testScheme)) ext := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, Spec: ocv1.ClusterExtensionSpec{Namespace: "test-namespace"}, } ctrl := gomock.NewController(t) brb := mockapplier.NewMockClusterObjectSetGenerator(ctrl) @@ -1482,7 +1482,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, ocv1.AddToScheme(testScheme)) ext := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, Spec: ocv1.ClusterExtensionSpec{Namespace: "test-namespace"}, } expectedRelease := &release.Release{ Name: "test123", @@ -1579,7 +1579,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, ocv1.AddToScheme(testScheme)) ext := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, Spec: ocv1.ClusterExtensionSpec{Namespace: "test-namespace"}, } ctrl := gomock.NewController(t) // GenerateRevisionFromHelmRelease should NOT be called when no deployed release exists @@ -1626,7 +1626,7 @@ func TestBoxcutterStorageMigrator(t *testing.T) { require.NoError(t, ocv1.AddToScheme(testScheme)) ext := &ocv1.ClusterExtension{ - ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, Spec: ocv1.ClusterExtensionSpec{Namespace: "test-namespace"}, } ctrl := gomock.NewController(t) brb := mockapplier.NewMockClusterObjectSetGenerator(ctrl) @@ -1650,4 +1650,31 @@ func TestBoxcutterStorageMigrator(t *testing.T) { err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) require.NoError(t, err) }) + + t.Run("skips migration for managed namespace mode (empty spec.namespace)", func(t *testing.T) { + testScheme := runtime.NewScheme() + require.NoError(t, ocv1.AddToScheme(testScheme)) + + ext := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: "test123"}, + } + ctrl := gomock.NewController(t) + // A managed-namespace extension never had a Helm release, so migration must be a no-op: + // no List, no action client, and no revision generation. No expectations are set, so + // gomock fails the test if any of these are called. + brb := mockapplier.NewMockClusterObjectSetGenerator(ctrl) + mag := newMockActionGetter(ctrl, mockActionGetterConfig{}) + mockClient := mockctrlclient.NewMockClient(ctrl) + + sm := &applier.BoxcutterStorageMigrator{ + RevisionGenerator: brb, + ActionClientGetter: mag, + Client: mockClient, + Scheme: testScheme, + FieldOwner: "test-owner", + } + + err := sm.Migrate(t.Context(), ext, map[string]string{"my-label": "my-value"}) + require.NoError(t, err) + }) } diff --git a/internal/operator-controller/applier/provider.go b/internal/operator-controller/applier/provider.go index 77343cf9da..d4db0a8570 100644 --- a/internal/operator-controller/applier/provider.go +++ b/internal/operator-controller/applier/provider.go @@ -29,11 +29,12 @@ type ManifestProvider interface { // RegistryV1ManifestProvider generates the manifests that should be installed for a registry+v1 bundle // given the user specified configuration given by the ClusterExtension API surface type RegistryV1ManifestProvider struct { - BundleRenderer render.BundleRenderer - CertificateProvider render.CertificateProvider - IsWebhookSupportEnabled bool - IsSingleOwnNamespaceEnabled bool - IsDeploymentConfigEnabled bool + BundleRenderer render.BundleRenderer + CertificateProvider render.CertificateProvider + IsWebhookSupportEnabled bool + IsSingleOwnNamespaceEnabled bool + IsDeploymentConfigEnabled bool + IsNamespaceManagementEnabled bool } func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) { @@ -67,9 +68,19 @@ func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtens return nil, fmt.Errorf("unsupported bundle: bundle must support at least one of [AllNamespaces SingleNamespace OwnNamespace] install modes") } + if ext.Spec.Namespace == "" && !r.IsNamespaceManagementEnabled { + return nil, errorutil.NewTerminalError(ocv1.ReasonInvalidConfiguration, fmt.Errorf("spec.namespace is required unless the BoxcutterRuntime feature gate is enabled")) + } + opts := []render.Option{ render.WithCertificateProvider(r.CertificateProvider), - render.WithSelfManagedInstallNamespace(ext.Spec.Namespace), + } + + // When the user set spec.namespace, render into that caller-managed (already-existing) + // namespace and do not emit a Namespace object. Otherwise the renderer resolves the + // bundle's system-managed namespace and emits the Namespace object for it. + if ext.Spec.Namespace != "" { + opts = append(opts, render.WithSelfManagedInstallNamespace(ext.Spec.Namespace)) } // Always validate inline config when present so that disabled features produce diff --git a/internal/operator-controller/applier/provider_test.go b/internal/operator-controller/applier/provider_test.go index 6fb9760417..d1b26faf54 100644 --- a/internal/operator-controller/applier/provider_test.go +++ b/internal/operator-controller/applier/provider_test.go @@ -2,6 +2,7 @@ package applier_test import ( "errors" + "io/fs" "testing" "testing/fstest" @@ -139,17 +140,7 @@ func Test_RegistryV1ManifestProvider_Integration(t *testing.T) { provider := applier.RegistryV1ManifestProvider{ BundleRenderer: registryv1.Renderer, } - bundleFS := bundlefs.Builder().WithPackageName("test"). - WithCSV(bundlecsv.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()). - WithBundleResource("service.yaml", &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: corev1.SchemeGroupVersion.String(), - Kind: "Service", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-service", - }, - }).Build() + bundleFS := newAllNamespacesBundleFS(t) ext := &ocv1.ClusterExtension{ Spec: ocv1.ClusterExtensionSpec{ Namespace: "install-namespace", @@ -174,6 +165,115 @@ func Test_RegistryV1ManifestProvider_Integration(t *testing.T) { require.Equal(t, []client.Object{exp}, objs) }) + + t.Run("emits a system-managed Namespace object when spec.namespace is empty", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: registryv1.Renderer, + IsNamespaceManagementEnabled: true, + } + bundleFS := bundlefs.Builder().WithPackageName("test"). + WithCSV(bundlecsv.Builder(). + WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces). + WithAnnotations(map[string]string{ + render.AnnotationSuggestedNamespaceTemplate: `{"metadata":{"name":"managed-ns","labels":{"pod-security.kubernetes.io/enforce":"privileged"},"annotations":{"example.com/note":"hello"}}}`, + }).Build()). + WithBundleResource("service.yaml", &corev1.Service{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Service"}, + ObjectMeta: metav1.ObjectMeta{Name: "test-service"}, + }).Build() + // No spec.namespace -> system-managed: the renderer resolves the name from + // bundle annotations and emits the Namespace object. + ext := &ocv1.ClusterExtension{} + + objs, err := provider.Get(bundleFS, ext) + require.NoError(t, err) + require.NotEmpty(t, objs) + + t.Log("by checking the Namespace object is emitted first") + ns := objs[0] + require.Equal(t, "Namespace", ns.GetObjectKind().GroupVersionKind().Kind) + require.Equal(t, "managed-ns", ns.GetName()) + + t.Log("by checking template labels and annotations are applied") + require.Equal(t, "privileged", ns.GetLabels()["pod-security.kubernetes.io/enforce"]) + require.Equal(t, "hello", ns.GetAnnotations()["example.com/note"]) + }) + + t.Run("does not emit a Namespace object when spec.namespace is set", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: registryv1.Renderer, + } + bundleFS := newAllNamespacesBundleFS(t) + ext := &ocv1.ClusterExtension{Spec: ocv1.ClusterExtensionSpec{Namespace: "install-namespace"}} + + objs, err := provider.Get(bundleFS, ext) + require.NoError(t, err) + for _, o := range objs { + require.NotEqual(t, "Namespace", o.GetObjectKind().GroupVersionKind().Kind, "no Namespace should be emitted when spec.namespace is set") + } + }) +} + +func Test_RegistryV1ManifestProvider_BoxcutterRuntimeGate(t *testing.T) { + t.Run("rejects empty spec.namespace when the BoxcutterRuntime feature gate is disabled", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: registryv1.Renderer, + IsNamespaceManagementEnabled: false, + } + bundleFS := newAllNamespacesBundleFS(t) + ext := &ocv1.ClusterExtension{} + + _, err := provider.Get(bundleFS, ext) + require.Error(t, err) + require.Contains(t, err.Error(), "spec.namespace is required unless the BoxcutterRuntime feature gate is enabled") + require.ErrorIs(t, err, reconcile.TerminalError(nil), "namespace gate error should be terminal") + }) + + t.Run("allows empty spec.namespace and renders a managed Namespace when the BoxcutterRuntime feature gate is enabled", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: registryv1.Renderer, + IsNamespaceManagementEnabled: true, + } + bundleFS := newAllNamespacesBundleFS(t) + ext := &ocv1.ClusterExtension{} + + objs, err := provider.Get(bundleFS, ext) + require.NoError(t, err) + require.Contains(t, collectKinds(objs), "Namespace") + }) + + t.Run("ignores the BoxcutterRuntime feature gate when spec.namespace is set", func(t *testing.T) { + provider := applier.RegistryV1ManifestProvider{ + BundleRenderer: registryv1.Renderer, + IsNamespaceManagementEnabled: false, + } + bundleFS := newAllNamespacesBundleFS(t) + ext := &ocv1.ClusterExtension{Spec: ocv1.ClusterExtensionSpec{Namespace: "install-namespace"}} + + objs, err := provider.Get(bundleFS, ext) + require.NoError(t, err) + require.NotContains(t, collectKinds(objs), "Namespace") + }) +} + +// newAllNamespacesBundleFS returns a minimal registry+v1 bundle FS that supports the +// AllNamespaces install mode and includes a single Service resource named "test-service". +func newAllNamespacesBundleFS(t *testing.T) fs.FS { + t.Helper() + return bundlefs.Builder().WithPackageName("test"). + WithCSV(bundlecsv.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()). + WithBundleResource("service.yaml", &corev1.Service{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String(), Kind: "Service"}, + ObjectMeta: metav1.ObjectMeta{Name: "test-service"}, + }).Build() +} + +func collectKinds(objs []client.Object) []string { + kinds := make([]string, 0, len(objs)) + for _, o := range objs { + kinds = append(kinds, o.GetObjectKind().GroupVersionKind().Kind) + } + return kinds } func Test_RegistryV1ManifestProvider_APIServiceSupport(t *testing.T) { diff --git a/internal/operator-controller/controllers/clusterextension_controller_test.go b/internal/operator-controller/controllers/clusterextension_controller_test.go index 2637457752..5f721adb8e 100644 --- a/internal/operator-controller/controllers/clusterextension_controller_test.go +++ b/internal/operator-controller/controllers/clusterextension_controller_test.go @@ -15,11 +15,14 @@ import ( "go.uber.org/mock/gomock" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/client-go/kubernetes/fake" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -979,6 +982,98 @@ func TestValidateClusterExtension(t *testing.T) { } } +func TestValidateInstallNamespace(t *testing.T) { + tests := []struct { + name string + specNamespace string + namespaceObjects []runtime.Object + expectError bool + errorMessageIncludes string + }{ + { + name: "user-provided namespace exists", + specNamespace: "existing-ns", + namespaceObjects: []runtime.Object{ + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "existing-ns"}}, + }, + }, + { + name: "user-provided namespace not found", + specNamespace: "missing-ns", + expectError: true, + errorMessageIncludes: `namespace "missing-ns" not found`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + fakeClient := fake.NewClientset(tt.namespaceObjects...) + + cl := newClient(t) + reconciler := &controllers.ClusterExtensionReconciler{ + Client: cl, + ReconcileSteps: controllers.ReconcileSteps{ + controllers.HandleFinalizers(crfinalizer.NewFinalizers()), + controllers.ValidateInstallNamespace(fakeClient.CoreV1()), + }, + } + + extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} + clusterExtension := &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, + Spec: ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{PackageName: "test-package"}, + }, + Namespace: tt.specNamespace, + ServiceAccount: ocv1.ServiceAccountReference{ //nolint:staticcheck // deprecated field used in test + Name: "test-sa", + }, + }, + } + require.NoError(t, cl.Create(ctx, clusterExtension)) + + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) + require.Equal(t, ctrl.Result{}, res) + if !tt.expectError { + require.NoError(t, err) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) + return + } + + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMessageIncludes) + + require.NoError(t, cl.Get(ctx, extKey, clusterExtension)) + progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(t, progressingCond) + // A missing namespace is retryable (not terminal): the user can create it and + // the next reconcile succeeds, so Progressing stays True with Reason=Retrying. + require.Equal(t, metav1.ConditionTrue, progressingCond.Status) + require.Equal(t, ocv1.ReasonRetrying, progressingCond.Reason) + require.Contains(t, progressingCond.Message, tt.errorMessageIncludes) + require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) + }) + } +} + +// The CRD still requires a non-empty spec.namespace, so this case cannot be driven through the +// API server. Call the step directly to cover the system-managed short-circuit. +func TestValidateInstallNamespaceSkipsSystemManaged(t *testing.T) { + fakeClient := fake.NewClientset() + step := controllers.ValidateInstallNamespace(fakeClient.CoreV1()) + + res, err := step(context.Background(), nil, &ocv1.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{Name: "test-extension"}, + }) + + require.NoError(t, err) + require.Nil(t, res) + require.Empty(t, fakeClient.Actions(), "no namespace lookup should happen for a system-managed namespace") +} + func TestClusterExtensionApplierFailsWithBundleInstalled(t *testing.T) { // This test calls Reconcile twice: first with a successful applier, // then with a failing applier. We use gomock.InOrder to sequence the calls. diff --git a/internal/operator-controller/controllers/clusterextension_reconcile_steps.go b/internal/operator-controller/controllers/clusterextension_reconcile_steps.go index b07a5072f4..c541202d2d 100644 --- a/internal/operator-controller/controllers/clusterextension_reconcile_steps.go +++ b/internal/operator-controller/controllers/clusterextension_reconcile_steps.go @@ -21,8 +21,10 @@ import ( "errors" "fmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/finalizer" @@ -402,6 +404,35 @@ func UnpackBundle(i imageutil.Puller, cache imageutil.Cache) ReconcileStepFunc { } } +// ValidateInstallNamespace verifies that a user-provided spec.namespace exists. +// +// A missing namespace is recoverable — the user can create it — so it is surfaced as a retryable +// error rather than a terminal one: the next reconcile succeeds once the namespace exists. +// +// When spec.namespace is omitted the namespace is system-managed and the renderer creates it, so +// there is nothing to check. +func ValidateInstallNamespace(nsClient corev1client.NamespacesGetter) ReconcileStepFunc { + return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) { + if ext.Spec.Namespace == "" { + return nil, nil + } + + l := log.FromContext(ctx) + l.V(1).Info("validating user-provided namespace exists", "namespace", ext.Spec.Namespace) + + _, err := nsClient.Namespaces().Get(ctx, ext.Spec.Namespace, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + nsErr := fmt.Errorf("namespace %q not found; spec.namespace must reference an existing namespace", ext.Spec.Namespace) + setStatusProgressing(ext, nsErr) + return nil, nsErr + } + if err != nil { + return nil, fmt.Errorf("error checking namespace %q: %w", ext.Spec.Namespace, err) + } + return nil, nil + } +} + func ApplyBundle(a Applier) ReconcileStepFunc { return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) { l := log.FromContext(ctx)