diff --git a/cmd/deploy.go b/cmd/deploy.go index 1fc3cfa..0480cbf 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -20,7 +20,6 @@ import ( "github.com/stackrox/roxie/internal/env" "github.com/stackrox/roxie/internal/helpers" "github.com/stackrox/roxie/internal/imagetag" - "github.com/stackrox/roxie/internal/k8s" log "github.com/stackrox/roxie/internal/logger" "github.com/stackrox/roxie/internal/manifest" "github.com/stackrox/roxie/internal/roxieenv" @@ -235,14 +234,9 @@ func runDeploy(cmd *cobra.Command, args []string) error { return err } - // Cluster-config retrieval happens before the full config (and thus the configured deploy - // wait timeouts) is assembled, so give it its own short bounded context. - setupCtx, setupCancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer setupCancel() + clusterConfig := deployer.DefaultConfig() - clusterConfig := retrieveClusterConfigForComponents(setupCtx, components) - - deploySettings, err := assembleConfigForCommand(clusterConfig, deploySettingsFromArgs, skipUserConfig) + deploySettings, err := assembleConfigForCommand(&clusterConfig, deploySettingsFromArgs, skipUserConfig) if err != nil { return err } @@ -344,20 +338,23 @@ func runDeploy(cmd *cobra.Command, args []string) error { d.WaitForCentral(5 * time.Minute) } - if !dryRun { + if components.IncludesCentral() { roxieEnv := roxieenv.AssembleRoxieEnvironment(d.GetCentralDeploymentInfo()) - m := manifest.RoxieManifest{ + newManifest := manifest.RoxieManifest{ RoxieEnvironment: roxieEnv, - Config: deploySettings, + Config: manifest.RoxieManifestConfig{ + Roxie: deploySettings.Roxie, + Central: deploySettings.Central, + }, } - if err := manifest.CreateManifestSecretOnCluster(ctx, m); err != nil { + if err := manifest.CreateManifestSecretOnCluster(ctx, newManifest); err != nil { log.Warningf("Failed to save roxie manifest: %v", err) } - } - if components.IncludesCentral() && envrc == "" { - if err := spawnSubshellForDeployerEnv(deploySettings.Roxie, d); err != nil { - return fmt.Errorf("failed to spawn subshell: %w", err) + if envrc == "" { + if err := spawnSubshellForDeployerEnv(deploySettings.Roxie, d); err != nil { + return fmt.Errorf("failed to spawn subshell: %w", err) + } } } @@ -402,30 +399,6 @@ func validateContainerizedCredentials(registry string, clusterType types.Cluster return nil } -func retrieveClusterConfigForComponents( - ctx context.Context, - components component.Component, -) *deployer.Config { - clusterManifest, err := manifest.LoadManifestSecret(ctx) - if err != nil { - if errors.Is(err, k8s.ErrResourceNotFound) { - log.Dim("No existing manifest found on cluster, starting from defaults") - } else { - log.Warningf("Failed to load manifest from cluster: %v", err) - } - return nil // Let's try to continue without manifest - } - - if components == component.SecuredCluster && clusterManifest.Config.SecuredCluster.Spec != nil { - // Reusing these fields from the config stored on the cluster would cause problems - // during repeating sensor deploy & teardown cycles. - delete(clusterManifest.Config.SecuredCluster.Spec, "clusterName") - delete(clusterManifest.Config.SecuredCluster.Spec, "centralEndpoint") - } - - return &clusterManifest.Config -} - func configureConfig(components component.Component, deploySettings *deployer.Config) error { if deploySettings.Roxie.ClusterType == "" { clusterType := env.GetAutoDetectedClusterType() diff --git a/cmd/main.go b/cmd/main.go index 52acea8..84eb976 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -136,7 +136,7 @@ func init() { ) rootCmd.AddCommand(newDeployCmd(&deploySettingsFromArgs)) - rootCmd.AddCommand(newTeardownCmd(&deploySettingsFromArgs)) + rootCmd.AddCommand(newTeardownCmd()) rootCmd.AddCommand(newShellCmd()) rootCmd.AddCommand(newVersionCmd()) rootCmd.AddCommand(newEnvCmd()) diff --git a/cmd/shell.go b/cmd/shell.go index a403332..33f704f 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -72,7 +72,7 @@ func runShell(args []string) error { log.Dim("roxie manifest loaded") configBase := m.Config - config, err := assembleConfigForCommand(&configBase, deploySettingsFromArgs, skipUserConfig) + config, err := assembleConfigForCommand(configBase.AsConfig(), deploySettingsFromArgs, skipUserConfig) if err != nil { return err } diff --git a/cmd/teardown.go b/cmd/teardown.go index 0b5c09d..ef44418 100644 --- a/cmd/teardown.go +++ b/cmd/teardown.go @@ -15,7 +15,7 @@ import ( "github.com/stackrox/roxie/internal/manifest" ) -func newTeardownCmd(settings *deployer.Config) *cobra.Command { +func newTeardownCmd() *cobra.Command { cmd := &cobra.Command{ Use: "teardown [component]", Short: "Teardown ACS components", @@ -25,17 +25,9 @@ func newTeardownCmd(settings *deployer.Config) *cobra.Command { RunE: runTeardown, } - registerFlag(cmd, settings, "single-namespace", "Deploy all components in a single namespace ('stackrox')", - withNoOptDefVal("true"), - withApplyFnBool(func(config *deployer.Config, val bool) error { - // We do not support --single-namespace=false as of now. - if val { - config.Central.Namespace = sharedNamespace - config.SecuredCluster.Namespace = sharedNamespace - } - return nil - }), - ) + // Accepted for backwards compatibility but ignored — teardown now discovers namespaces from the cluster. + cmd.Flags().Bool("single-namespace", false, "Deprecated: teardown discovers namespaces automatically") + _ = cmd.Flags().MarkDeprecated("single-namespace", "teardown now discovers namespaces automatically") return cmd } @@ -60,19 +52,18 @@ func runTeardown(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() - var clusterConfig *deployer.Config - clusterManifest, err := manifest.LoadManifestSecret(ctx) + // Load manifest from cluster for add-on config. Namespaces are discovered + // directly from the cluster, but add-on definitions live in the Central + // config and are not discoverable — we need the manifest to know which + // add-ons were deployed so we can tear them down. + var config deployer.Config + m, err := manifest.LoadManifestSecret(ctx) if errors.Is(err, k8s.ErrResourceNotFound) { log.Infof("No roxie manifest found in cluster, proceeding without it") } else if err != nil { log.Warningf("Failed to load manifest from cluster: %v", err) } else { - clusterConfig = &clusterManifest.Config - } - - deploySettings, err := assembleConfigForCommand(clusterConfig, deploySettingsFromArgs, skipUserConfig) - if err != nil { - return err + config = *m.Config.AsConfig() } d, err := deployer.New() @@ -81,7 +72,7 @@ func runTeardown(cmd *cobra.Command, args []string) error { } defer d.Cleanup() - d.SetConfig(deploySettings) + d.SetConfig(config) if err := d.Teardown(ctx, components); err != nil { return fmt.Errorf("teardown failed: %w", err) diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 84dab48..46af0bd 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -147,11 +147,11 @@ func (d *Deployer) deleteCentralResources(ctx context.Context) error { return nil } -func (d *Deployer) deleteSecuredClusterResources(ctx context.Context) error { +func (d *Deployer) deleteSecuredClusterResources(ctx context.Context, namespace string) error { log.Info("Deleting SecuredCluster resources") crExists := true - if _, err := k8s.RetrieveResourceFromCluster(ctx, d.config.SecuredCluster.Namespace, "securedcluster", "stackrox-secured-cluster-services"); err != nil { + if _, err := k8s.RetrieveResourceFromCluster(ctx, namespace, "securedcluster", securedClusterCrName); err != nil { if !k8s.IsResourceNotFound(err) { return fmt.Errorf("retrieving SecuredCluster CR: %w", err) } @@ -160,12 +160,12 @@ func (d *Deployer) deleteSecuredClusterResources(ctx context.Context) error { if crExists { log.Info("Removing any pause-reconcile annotation from SecuredCluster") - if err := d.removePauseReconcileAnnotation(ctx, "securedcluster", "stackrox-secured-cluster-services", d.config.SecuredCluster.Namespace); err != nil { + if err := d.removePauseReconcileAnnotation(ctx, "securedcluster", securedClusterCrName, namespace); err != nil { return err } log.Debug("Removed any pause-reconcile annotation from SecuredCluster") - err := d.deleteResource(ctx, d.config.SecuredCluster.Namespace, "securedcluster", "stackrox-secured-cluster-services", "--wait") + err := d.deleteResource(ctx, namespace, "securedcluster", securedClusterCrName, "--wait") if err != nil { return err } @@ -183,9 +183,7 @@ func (d *Deployer) deleteSecuredClusterResources(ctx context.Context) error { } { log.Dimf("Attempting to delete %s/%s", resource.Kind, resource.Name) if resource.OwnerName != "" { - // Avoid deletion if the resource does not have the expected owner. - // (e.g. in case central and secured cluster are deployed into the same namespace). - obj, err := k8s.RetrieveResourceFromCluster(ctx, d.config.SecuredCluster.Namespace, resource.Kind, resource.Name) + obj, err := k8s.RetrieveResourceFromCluster(ctx, namespace, resource.Kind, resource.Name) if err != nil { if !k8s.IsResourceNotFound(err) { log.Warningf("Failed to retrieve %s/%s for owner checking: %v. Skipping deletion. Deployment might be affected.", resource.Kind, resource.Name, err) @@ -197,7 +195,7 @@ func (d *Deployer) deleteSecuredClusterResources(ctx context.Context) error { continue } } - if err := d.deleteResource(ctx, d.config.SecuredCluster.Namespace, resource.Kind, resource.Name); err != nil { + if err := d.deleteResource(ctx, namespace, resource.Kind, resource.Name); err != nil { return fmt.Errorf("failed to delete %s/%s: %w", resource.Kind, resource.Name, err) } } @@ -497,19 +495,21 @@ func (d *Deployer) Teardown(ctx context.Context, components component.Component) } func (d *Deployer) teardownCentral(ctx context.Context) error { - log.Infof("🗑️ Tearing down central in namespace %s", d.config.Central.Namespace) - - if !d.namespaceExists(d.config.Central.Namespace) { - log.Infof("Namespace %s doesn't exist, skipping", d.config.Central.Namespace) + namespaces, err := d.discoverCRNamespaces(ctx, "central", centralCrName) + if err != nil { + log.Infof("No Central CR found on cluster, skipping Central teardown") return nil } + d.config.Central.Namespace = namespaces[0] + + log.Infof("🗑️ Tearing down central in namespace %s", d.config.Central.Namespace) d.portForward.Stop() d.stopDetachedPortForward() // Add pause-reconcile annotation to not have the operator interfere during resource deletion. - if d.doesResourceExist(ctx, "central", "stackrox-central-services", d.config.Central.Namespace) { - if err := d.addPauseReconcileAnnotation(ctx, "central", "stackrox-central-services", d.config.Central.Namespace); err != nil { + if d.doesResourceExist(ctx, "central", centralCrName, d.config.Central.Namespace) { + if err := d.addPauseReconcileAnnotation(ctx, "central", centralCrName, d.config.Central.Namespace); err != nil { log.Warningf("Error adding pause-reconcile annotation: %v", err) } } @@ -524,27 +524,41 @@ func (d *Deployer) teardownCentral(ctx context.Context) error { } func (d *Deployer) teardownSecuredCluster(ctx context.Context) error { - log.Infof("🗑️ Tearing down secured cluster in namespace %s", d.config.SecuredCluster.Namespace) - - if !d.namespaceExists(d.config.SecuredCluster.Namespace) { - log.Infof("Namespace %s doesn't exist, skipping", d.config.SecuredCluster.Namespace) + namespaces, err := d.discoverCRNamespaces(ctx, "securedcluster", securedClusterCrName) + if err != nil { + log.Infof("No SecuredCluster CR found on cluster, skipping SecuredCluster teardown") return nil } - if d.doesResourceExist(ctx, "securedcluster", "stackrox-secured-cluster-services", d.config.SecuredCluster.Namespace) { - // Add pause-reconcile annotation to not have the operator interfere during resource deletion. - if err := d.addPauseReconcileAnnotation(ctx, "securedcluster", "stackrox-secured-cluster-services", d.config.SecuredCluster.Namespace); err != nil { + var wg sync.WaitGroup + wg.Add(len(namespaces)) + for _, ns := range namespaces { + go func() { + defer wg.Done() + if err := d.teardownSecuredClusterInNamespace(ctx, ns); err != nil { + log.Warningf("Error tearing down SecuredCluster in namespace %s: %v", ns, err) + } + }() + } + wg.Wait() + return nil +} + +func (d *Deployer) teardownSecuredClusterInNamespace(ctx context.Context, namespace string) error { + log.Infof("🗑️ Tearing down secured cluster in namespace %s", namespace) + + if d.doesResourceExist(ctx, "securedcluster", securedClusterCrName, namespace) { + if err := d.addPauseReconcileAnnotation(ctx, "securedcluster", securedClusterCrName, namespace); err != nil { log.Warningf("Error adding pause-reconcile annotation: %v", err) } } log.Info("⏳ Waiting for SecuredCluster resources to be fully deleted...") - err := d.deleteSecuredClusterResources(ctx) - if err != nil { - return fmt.Errorf("failed to delete SecuredCluster resources: %w", err) + if err := d.deleteSecuredClusterResources(ctx, namespace); err != nil { + return fmt.Errorf("failed to delete SecuredCluster resources in namespace %s: %w", namespace, err) } - log.Successf("✓ SecuredCluster resources in namespace %s have been deleted", d.config.SecuredCluster.Namespace) + log.Successf("✓ SecuredCluster resources in namespace %s have been deleted", namespace) return nil } @@ -646,6 +660,21 @@ func (d *Deployer) SetEnvrcFile(path string) { d.envrcFile = path } +func (d *Deployer) discoverCRNamespaces(ctx context.Context, crType, crName string) ([]string, error) { + result, err := k8s.RunKubectl(ctx, k8s.KubectlOptions{ + Args: []string{"get", crType, crName, "-A", "-o", "jsonpath={.items[*].metadata.namespace}"}, + MaxAttempts: 1, + }) + if err != nil { + return nil, fmt.Errorf("discovering %s/%s: %w", crType, crName, err) + } + raw := strings.TrimSpace(result.Stdout) + if raw == "" { + return nil, fmt.Errorf("%s/%s not found on cluster", crType, crName) + } + return strings.Fields(raw), nil +} + func (d *Deployer) doesResourceExist(ctx context.Context, resourceType, resourceName, namespace string) bool { _, err := k8s.RunKubectl(ctx, k8s.KubectlOptions{ Args: []string{ diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 09386cf..68f291d 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -28,7 +28,36 @@ const ( // We include the whole deployer Config for reproducibility purposes. type RoxieManifest struct { RoxieEnvironment types.RoxieEnvironment `yaml:"roxieEnvironment"` - Config deployer.Config `yaml:"config"` + Config RoxieManifestConfig `yaml:"config"` +} + +// RoxieManifestConfig stores the subset of deployer.Config that is persisted +// in the cluster manifest secret. Namespaces are NOT consumed from here — +// teardown discovers them directly from CRs on the cluster. The fields that +// are actively used: +// +// - Central.AddOns / Central.AvailableAddOns — teardown needs these to know +// which add-ons were deployed so it can tear them down (add-ons are +// lifecycle-aligned with Central and not discoverable from the cluster). +// - Central.Namespace — used by 'roxie shell' to fetch the CA cert from the +// central-tls secret, and to resolve the Central endpoint. +// - Central.Exposure — used by 'roxie shell' to determine the base URL shown +// in the subshell banner. +// - Roxie.HAProxy — used by 'roxie shell' to re-spawn the HAProxy sidecar +// for local HTTP access to Central. +type RoxieManifestConfig struct { + Roxie deployer.RoxieConfig `yaml:"roxie"` + Central deployer.CentralConfig `yaml:"central"` +} + +func (c *RoxieManifestConfig) AsConfig() *deployer.Config { + if c == nil { + return nil + } + return &deployer.Config{ + Roxie: c.Roxie, + Central: c.Central, + } } func manifestToSecret(m RoxieManifest) (*unstructured.Unstructured, error) { diff --git a/internal/manifest/manifest_integration_test.go b/internal/manifest/manifest_integration_test.go index c8e69fe..5a302a9 100644 --- a/internal/manifest/manifest_integration_test.go +++ b/internal/manifest/manifest_integration_test.go @@ -37,7 +37,7 @@ func TestCreateAndLoadManifest_Integration(t *testing.T) { RoxEndpoint: "localhost:8443", RoxUsername: "admin", }, - Config: deployer.Config{ + Config: RoxieManifestConfig{ Central: deployer.CentralConfig{ Namespace: "acs-central", },