From d5e96253f0bc95a7cfcb43199b55219d97254345 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 14 Apr 2026 12:01:22 +0200 Subject: [PATCH 1/4] Refactor CRD installation/deletion, skipping second CRD installation. --- internal/deployer/crds.go | 120 +++++++++++++++++++ internal/deployer/deploy_via_helm.go | 17 --- internal/deployer/deploy_via_operator.go | 35 +++++- internal/deployer/operator.go | 144 +---------------------- internal/deployer/operator_olm.go | 3 - 5 files changed, 156 insertions(+), 163 deletions(-) create mode 100644 internal/deployer/crds.go diff --git a/internal/deployer/crds.go b/internal/deployer/crds.go new file mode 100644 index 0000000..2238d25 --- /dev/null +++ b/internal/deployer/crds.go @@ -0,0 +1,120 @@ +package deployer + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" +) + +// requiredCRDs lists the CRDs required for ACS operator. +var requiredCRDs = []string{ + "centrals.platform.stackrox.io", + "securedclusters.platform.stackrox.io", + "securitypolicies.config.stackrox.io", +} + +// ensureCRDsInstalled ensures required CRDs exist, installing them from the provided bundle directory. +func (d *Deployer) ensureCRDsInstalled(ctx context.Context, bundleDir string) error { + var missing []string + for _, crd := range requiredCRDs { + _, err := d.runKubectl(ctx, KubectlOptions{ + Args: []string{"get", "crd", crd}, + }) + if err != nil { + missing = append(missing, crd) + } + } + + if len(missing) > 0 { + d.logger.Warningf("Missing CRDs detected (%s)", strings.Join(missing, ", ")) + + crdFiles, err := d.identifyCRDFileNames(bundleDir) + if err != nil { + return err + } + + return d.applyCRDsToCluster(ctx, crdFiles) + } + + return nil +} + +// identifyCRDFileNames identifies CRD files in the bundle directory. +func (d *Deployer) identifyCRDFileNames(bundleDir string) ([]string, error) { + var crdFiles []string + + err := filepath.Walk(bundleDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + if !strings.HasSuffix(info.Name(), ".yaml") && !strings.HasSuffix(info.Name(), ".yml") { + return nil + } + + // TODO(#91): The following detection logic does not seem particularly robust. We should + // probably parse the YAML and check api group and kind fields. + name := strings.ToLower(info.Name()) + if strings.Contains(name, "customresourcedefinition") || + strings.Contains(name, "platform.stackrox.io") || + strings.Contains(name, "config.stackrox.io") { + if strings.Contains(name, "clusterserviceversion") { + return nil + } + + content, err := os.ReadFile(path) + if err != nil { + return nil + } + + if strings.Contains(string(content), "kind: CustomResourceDefinition") { + crdFiles = append(crdFiles, path) + } + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk bundle directory: %w", err) + } + + return crdFiles, nil +} + +// applyCRDsToCluster applies CRD files to the cluster +func (d *Deployer) applyCRDsToCluster(ctx context.Context, crdFiles []string) error { + d.logger.Infof("Applying %d CRD(s) to cluster", len(crdFiles)) + + for _, crdFile := range crdFiles { + result, err := d.runKubectl(ctx, KubectlOptions{ + Args: []string{"apply", "-f", crdFile}, + }) + if err != nil { + d.logger.Errorf("kubectl stderr: %s", result.Stderr) + return fmt.Errorf("failed to apply CRD %s: %w\nStderr: %s", crdFile, err, result.Stderr) + } + + basename := filepath.Base(crdFile) + d.logger.Successf("โœ“ Successfully applied CRD %s", basename) + } + + return nil +} + +// deleteCRDs deletes RHACS CRDs from the cluster +func (d *Deployer) deleteCRDs(ctx context.Context) { + d.logger.Info("Deleting CRDs...") + + args := append([]string{"delete", "crd", "--ignore-not-found=true"}, requiredCRDs...) + d.runKubectl(ctx, KubectlOptions{ + Args: args, + IgnoreErrors: true, + }) +} diff --git a/internal/deployer/deploy_via_helm.go b/internal/deployer/deploy_via_helm.go index a410cdf..27c7510 100644 --- a/internal/deployer/deploy_via_helm.go +++ b/internal/deployer/deploy_via_helm.go @@ -518,20 +518,3 @@ func (d *Deployer) installSecuredClusterHelmChart(ctx context.Context, chartDir, d.logger.Success("โœ“ Helm chart installed") return nil } - -// deleteCRDs deletes ACS CRDs (used before Helm deployment) -func (d *Deployer) deleteCRDs(ctx context.Context) { - crds := []string{ - "centrals.platform.stackrox.io", - "securedclusters.platform.stackrox.io", - "securitypolicies.config.stackrox.io", - } - - d.logger.Info("Deleting CRDs...") - - args := append([]string{"delete", "crd", "--ignore-not-found=true"}, crds...) - d.runKubectl(ctx, KubectlOptions{ - Args: args, - IgnoreErrors: true, - }) -} diff --git a/internal/deployer/deploy_via_operator.go b/internal/deployer/deploy_via_operator.go index 4fe2134..af8918c 100644 --- a/internal/deployer/deploy_via_operator.go +++ b/internal/deployer/deploy_via_operator.go @@ -47,10 +47,6 @@ func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { return nil } - if err := d.ensureCRDsInstalled(ctx); err != nil { - return fmt.Errorf("failed to ensure CRDs installed: %w", err) - } - // Detect current operator deployment mode operatorExists, currentMode := d.detectOperatorDeploymentMode(ctx) needsDeployment := false @@ -93,12 +89,41 @@ func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { } if needsDeployment { + d.logger.Info("๐Ÿš€ Deploying operator via OLM...") + d.logger.Infof("Operator tag: %s", d.operatorTag) + if d.useOLM { + // OLM takes care of CRD installation. if err := d.deployOperatorViaOLM(ctx); err != nil { return fmt.Errorf("failed to deploy operator via OLM: %w", err) } } else { - if err := d.deployOperatorNonOLM(ctx); err != nil { + if d.useKonflux { + if err := d.ensureKonfluxImageRewriting(ctx); err != nil { + return fmt.Errorf("failed to configure Konflux image rewriting: %w", err) + } + } else { + if err := d.removeKonfluxImageRewriting(ctx); err != nil { + return fmt.Errorf("failed to remove Konflux ImageContentSourcePolicy: %v", err) + } + } + + bundleImage := d.getOperatorBundleImage() + d.logger.Infof("Bundle image: %s", bundleImage) + + bundleDir, err := d.downloadAndExtractOperatorBundle(ctx, bundleImage) + if err != nil { + return fmt.Errorf("failed to download operator bundle: %w", err) + } + defer d.cleanupTempDir(bundleDir, "operator bundle directory") + + // Install CRDs from the bundle + if err := d.ensureCRDsInstalled(ctx, bundleDir); err != nil { + return fmt.Errorf("failed to ensure CRDs installed: %w", err) + } + + // Deploy operator from the same bundle + if err := d.deployOperatorNonOLM(ctx, bundleDir); err != nil { return fmt.Errorf("failed to deploy operator: %w", err) } } diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index d76d224..c8e19a9 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -26,43 +26,9 @@ const ( operatorBundleImageReleaseRepo = "quay.io/rhacs-eng/release-operator-bundle" ) -var requiredCRDs = []string{ - "centrals.platform.stackrox.io", - "securedclusters.platform.stackrox.io", - "securitypolicies.config.stackrox.io", -} - -// deployOperatorNonOLM deploys the RHACS operator without OLM -func (d *Deployer) deployOperatorNonOLM(ctx context.Context) error { - d.logger.Infof("Operator tag: %s", d.operatorTag) - if d.useKonflux { - if err := d.ensureKonfluxImageRewriting(ctx); err != nil { - return fmt.Errorf("failed to configure Konflux image rewriting: %w", err) - } - } else { - if err := d.removeKonfluxImageRewriting(ctx); err != nil { - return fmt.Errorf("failed to remove Konflux ImageContentSourcePolicy: %v", err) - } - } - bundleImage := d.getOperatorBundleImage() - - bundleDir, err := d.downloadAndExtractOperatorBundle(ctx, bundleImage) - if err != nil { - return fmt.Errorf("failed to download operator bundle: %w", err) - } - defer d.cleanupTempDir(bundleDir, "operator bundle directory") - - d.logger.Infof("Bundle image: %s", bundleImage) - - crdFiles, err := d.identifyCRDFileNames(bundleDir) - if err != nil { - return err - } - - if err := d.applyCRDsToCluster(ctx, crdFiles); err != nil { - return err - } - +// deployOperatorNonOLM deploys the RHACS operator without OLM from the provided bundle directory. +// The bundleDir should contain the extracted operator bundle with CSV and other manifests. +func (d *Deployer) deployOperatorNonOLM(ctx context.Context, bundleDir string) error { if err := d.deployOperatorFromCSV(ctx, bundleDir); err != nil { return err } @@ -90,107 +56,6 @@ func (d *Deployer) downloadAndExtractOperatorBundle(ctx context.Context, bundleI return bundleDir, nil } -// identifyCRDFileNames identifies CRD files in the bundle directory -func (d *Deployer) identifyCRDFileNames(bundleDir string) ([]string, error) { - var crdFiles []string - - err := filepath.Walk(bundleDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if info.IsDir() { - return nil - } - - if !strings.HasSuffix(info.Name(), ".yaml") && !strings.HasSuffix(info.Name(), ".yml") { - return nil - } - - // TODO(#91): The following detection logic does not seem particularly robust. We should - // probably parse the YAML and check api group and kind fields. - name := strings.ToLower(info.Name()) - if strings.Contains(name, "customresourcedefinition") || - strings.Contains(name, "platform.stackrox.io") || - strings.Contains(name, "config.stackrox.io") { - if strings.Contains(name, "clusterserviceversion") { - return nil - } - - content, err := os.ReadFile(path) - if err != nil { - return nil - } - - if strings.Contains(string(content), "kind: CustomResourceDefinition") { - crdFiles = append(crdFiles, path) - } - } - - return nil - }) - - if err != nil { - return nil, fmt.Errorf("failed to walk bundle directory: %w", err) - } - - return crdFiles, nil -} - -// applyCRDsToCluster applies CRD files to the cluster -func (d *Deployer) applyCRDsToCluster(ctx context.Context, crdFiles []string) error { - d.logger.Infof("Applying %d CRD(s) to cluster", len(crdFiles)) - - for _, crdFile := range crdFiles { - result, err := d.runKubectl(ctx, KubectlOptions{ - Args: []string{"apply", "-f", crdFile}, - }) - if err != nil { - d.logger.Errorf("kubectl stderr: %s", result.Stderr) - return fmt.Errorf("failed to apply CRD %s: %w\nStderr: %s", crdFile, err, result.Stderr) - } - - basename := filepath.Base(crdFile) - d.logger.Successf("โœ“ Successfully applied CRD %s", basename) - } - - return nil -} - -// ensureCRDsInstalled ensures required CRDs exist -func (d *Deployer) ensureCRDsInstalled(ctx context.Context) error { - var missing []string - for _, crd := range requiredCRDs { - _, err := d.runKubectl(ctx, KubectlOptions{ - Args: []string{"get", "crd", crd}, - }) - if err != nil { - missing = append(missing, crd) - } - } - - if len(missing) > 0 { - bundleImage := d.getOperatorBundleImage() - d.logger.Warningf("Missing CRDs detected (%s)", strings.Join(missing, ", ")) - d.logger.Warningf("Fetching bundle %s", bundleImage) - - bundleDir, err := d.downloadAndExtractOperatorBundle(ctx, bundleImage) - if err != nil { - return err - } - defer d.cleanupTempDir(bundleDir, "CRD bundle directory") - - crdFiles, err := d.identifyCRDFileNames(bundleDir) - if err != nil { - return err - } - - return d.applyCRDsToCluster(ctx, crdFiles) - } - - return nil -} - func (d *Deployer) getOperatorBundleImage() string { if d.useKonflux { d.logger.Infof("Using Konflux-built operator bundle image") @@ -632,6 +497,9 @@ func (d *Deployer) teardownOperatorNonOLM(ctx context.Context) error { }) } + // Delete CRDs to ensure clean redeployment with updated CRD versions. + d.deleteCRDs(ctx) + if err := d.waitForNamespaceDeletion(operatorNamespace); err != nil { d.logger.Warningf("Namespace %s deletion incomplete: %v", operatorNamespace, err) } diff --git a/internal/deployer/operator_olm.go b/internal/deployer/operator_olm.go index 94b33f0..50f2865 100644 --- a/internal/deployer/operator_olm.go +++ b/internal/deployer/operator_olm.go @@ -29,9 +29,6 @@ const ( // deployOperatorViaOLM deploys the RHACS operator using OLM. func (d *Deployer) deployOperatorViaOLM(ctx context.Context) error { - d.logger.Info("๐Ÿš€ Deploying operator via OLM...") - d.logger.Infof("Operator tag: %s", d.operatorTag) - if err := d.checkOLMInstalled(ctx); err != nil { return err } From 445a172a319ee7c827a80a2e7686e27c146ec57c Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Tue, 14 Apr 2026 14:09:17 +0200 Subject: [PATCH 2/4] Remove deployOperatorNonOLM indirection --- internal/deployer/deploy_via_operator.go | 6 ++---- internal/deployer/operator.go | 10 ---------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/internal/deployer/deploy_via_operator.go b/internal/deployer/deploy_via_operator.go index af8918c..274e213 100644 --- a/internal/deployer/deploy_via_operator.go +++ b/internal/deployer/deploy_via_operator.go @@ -117,14 +117,12 @@ func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { } defer d.cleanupTempDir(bundleDir, "operator bundle directory") - // Install CRDs from the bundle if err := d.ensureCRDsInstalled(ctx, bundleDir); err != nil { return fmt.Errorf("failed to ensure CRDs installed: %w", err) } - // Deploy operator from the same bundle - if err := d.deployOperatorNonOLM(ctx, bundleDir); err != nil { - return fmt.Errorf("failed to deploy operator: %w", err) + if err := d.deployOperatorFromCSV(ctx, bundleDir); err != nil { + return fmt.Errorf("failed to deploy operator from CSV: %w", err) } } } diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index c8e19a9..bfcc6e0 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -26,16 +26,6 @@ const ( operatorBundleImageReleaseRepo = "quay.io/rhacs-eng/release-operator-bundle" ) -// deployOperatorNonOLM deploys the RHACS operator without OLM from the provided bundle directory. -// The bundleDir should contain the extracted operator bundle with CSV and other manifests. -func (d *Deployer) deployOperatorNonOLM(ctx context.Context, bundleDir string) error { - if err := d.deployOperatorFromCSV(ctx, bundleDir); err != nil { - return err - } - - return nil -} - // downloadAndExtractOperatorBundle downloads and extracts the operator bundle func (d *Deployer) downloadAndExtractOperatorBundle(ctx context.Context, bundleImage string) (string, error) { bundleDir, err := os.MkdirTemp("", "stackrox-operator-bundle-") From 9dd175522d177dfc5216402be26fb647f3459434 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier <111092021+mclasmeier@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:51:58 +0200 Subject: [PATCH 3/4] Update internal/deployer/deploy_via_operator.go Co-authored-by: Marcin Owsiany --- internal/deployer/deploy_via_operator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/deployer/deploy_via_operator.go b/internal/deployer/deploy_via_operator.go index 274e213..a62d559 100644 --- a/internal/deployer/deploy_via_operator.go +++ b/internal/deployer/deploy_via_operator.go @@ -89,7 +89,7 @@ func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { } if needsDeployment { - d.logger.Info("๐Ÿš€ Deploying operator via OLM...") + d.logger.Info("๐Ÿš€ Deploying operator...") d.logger.Infof("Operator tag: %s", d.operatorTag) if d.useOLM { From ceb771f01fca02a888804e2a09bd5ca323a4fb79 Mon Sep 17 00:00:00 2001 From: Moritz Clasmeier Date: Wed, 15 Apr 2026 19:22:50 +0200 Subject: [PATCH 4/4] wip --- internal/deployer/deploy_via_operator.go | 13 +++---------- internal/deployer/deployer.go | 14 ++++---------- internal/deployer/operator.go | 13 +++++++++++-- internal/deployer/operator_olm.go | 1 + 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/internal/deployer/deploy_via_operator.go b/internal/deployer/deploy_via_operator.go index a62d559..2951077 100644 --- a/internal/deployer/deploy_via_operator.go +++ b/internal/deployer/deploy_via_operator.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/stackrox/roxie/internal/component" "github.com/stackrox/roxie/internal/env" "github.com/stackrox/roxie/internal/helpers" "gopkg.in/yaml.v3" @@ -76,16 +77,8 @@ func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { } if needsTeardown { - // Perform teardown for the current mode - if currentMode == OperatorModeOLM { - if err := d.teardownOperatorOLM(ctx); err != nil { - return fmt.Errorf("failed to teardown OLM operator: %w", err) - } - } else { - if err := d.teardownOperatorNonOLM(ctx); err != nil { - return fmt.Errorf("failed to teardown non-OLM operator: %w", err) - } - } + // It is a prerequisite that all CRs are removed before the operator is removed to avoid blocking during teardown. + d.Teardown(ctx, component.All) } if needsDeployment { diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 6597cd2..c831ae0 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -612,11 +612,8 @@ func (d *Deployer) prepareCredentials() error { func (d *Deployer) deployCentral(ctx context.Context, resources, exposure string) error { d.logger.Infof("Deploying Central to namespace %s", d.centralNamespace) - if d.namespaceExists(d.centralNamespace) { - d.logger.Info("Existing Central deployment found, tearing down...") - if err := d.teardownCentral(ctx); err != nil { - d.logger.Warningf("Error during teardown: %v", err) - } + if err := d.Teardown(ctx, component.Both); err != nil { + d.logger.Warningf("Error during teardown: %v", err) } portForwardWanted := d.portForwardEnabled @@ -645,11 +642,8 @@ func (d *Deployer) deployCentral(ctx context.Context, resources, exposure string func (d *Deployer) deploySecuredCluster(ctx context.Context, resources string) error { d.logger.Infof("Deploying SecuredCluster to namespace %s", d.sensorNamespace) - if d.namespaceExists(d.sensorNamespace) { - d.logger.Info("Existing SecuredCluster deployment found, tearing down...") - if err := d.teardownSecuredCluster(ctx); err != nil { - d.logger.Warningf("Error during teardown: %v", err) - } + if err := d.Teardown(ctx, component.SecuredCluster); err != nil { + d.logger.Warningf("Error during teardown: %v", err) } if d.useHelm { diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index bfcc6e0..9d5b663 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -463,6 +463,7 @@ func generateClusterName() string { } // teardownOperatorNonOLM removes the operator when installed without OLM. +// Requires that all CRs are removed beforehand. func (d *Deployer) teardownOperatorNonOLM(ctx context.Context) error { d.logger.Info("๐Ÿงน Tearing down operator deployed without OLM...") @@ -499,6 +500,7 @@ func (d *Deployer) teardownOperatorNonOLM(ctx context.Context) error { } // teardownOperator removes the operator if it exists, detecting the deployment mode automatically. +// Requires that all CRs are removed beforehand. func (d *Deployer) teardownOperator(ctx context.Context) error { operatorExists, operatorMode := d.detectOperatorDeploymentMode(ctx) if !operatorExists { @@ -507,7 +509,14 @@ func (d *Deployer) teardownOperator(ctx context.Context) error { } if operatorMode == OperatorModeOLM { - return d.teardownOperatorOLM(ctx) + if err := d.teardownOperatorOLM(ctx); err != nil { + return fmt.Errorf("failed to teardown OLM operator: %w", err) + } + return nil } - return d.teardownOperatorNonOLM(ctx) + + if err := d.teardownOperatorNonOLM(ctx); err != nil { + return fmt.Errorf("failed to teardown non-OLM operator: %w", err) + } + return nil } diff --git a/internal/deployer/operator_olm.go b/internal/deployer/operator_olm.go index 50f2865..86ace96 100644 --- a/internal/deployer/operator_olm.go +++ b/internal/deployer/operator_olm.go @@ -363,6 +363,7 @@ func (d *Deployer) detectOperatorDeploymentMode(ctx context.Context) (bool, Oper } // teardownOperatorOLM removes the operator when installed via OLM. +// Requires that all CRs are removed beforehand. func (d *Deployer) teardownOperatorOLM(ctx context.Context) error { d.logger.Info("๐Ÿงน Tearing down operator deployed via OLM...")