diff --git a/cmd/deploy.go b/cmd/deploy.go index ab61ac7e..5ae7a3e1 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -40,6 +40,7 @@ Examples: cmd.Flags().StringVar(&resources, "resources", "auto", "Resource sizing preset (auto=cluster-based, medium, small)") cmd.Flags().StringVar(&shell, "shell", "", "Shell to spawn after Central deployment") cmd.Flags().StringVar(&envrc, "envrc", "", "Write environment to file instead of spawning sub-shell") + cmd.Flags().BoolVar(&singleNamespace, "single-namespace", false, "Deploy all components in a single namespace ('stackrox' by default)") return cmd } @@ -135,6 +136,7 @@ func runDeploy(cmd *cobra.Command, args []string) error { d.SetEarlyReadiness(earlyReadiness) d.SetPortForwardingEnabled(portForwardEnabledFinal) d.SetPauseReconciliation(pauseReconciliation) + d.SetSingleNamespace(singleNamespace) // Resolve "auto" resources based on cluster type resolvedResources := resources diff --git a/cmd/main.go b/cmd/main.go index 9759912a..40e0e822 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,6 +22,7 @@ var ( resources string shell string envrc string + singleNamespace bool ) func main() { diff --git a/cmd/teardown.go b/cmd/teardown.go index a8027fe6..25e5b63f 100644 --- a/cmd/teardown.go +++ b/cmd/teardown.go @@ -21,6 +21,7 @@ func newTeardownCmd() *cobra.Command { } cmd.Flags().BoolVar(&helm, "helm", false, "Force teardown of Helm deployment") + cmd.Flags().BoolVar(&singleNamespace, "single-namespace", false, "Deploy all components in a single namespace ('stackrox' by default)") return cmd } @@ -40,6 +41,8 @@ func runTeardown(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to create deployer: %w", err) } + d.SetSingleNamespace(singleNamespace) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() diff --git a/internal/deployer/constants.go b/internal/deployer/constants.go index 47ccb926..2a852a6a 100644 --- a/internal/deployer/constants.go +++ b/internal/deployer/constants.go @@ -1,8 +1,8 @@ package deployer -var ( - internalCentralEndpoint = "central.acs-central.svc:443" +import "fmt" +var ( centralResourcesSmall = map[string]interface{}{ "requests": map[string]string{ "memory": "1Gi", @@ -183,3 +183,7 @@ var ( }, } ) + +func internalCentralEndpoint(namespace string) string { + return fmt.Sprintf("central.%s.svc:443", namespace) +} diff --git a/internal/deployer/deploy_via_operator.go b/internal/deployer/deploy_via_operator.go index 7af042a1..ab48c8cd 100644 --- a/internal/deployer/deploy_via_operator.go +++ b/internal/deployer/deploy_via_operator.go @@ -622,7 +622,7 @@ func (d *Deployer) createSecuredClusterCR(clusterName, resources string) (map[st }, "spec": map[string]interface{}{ "clusterName": clusterName, - "centralEndpoint": internalCentralEndpoint, + "centralEndpoint": internalCentralEndpoint(d.centralNamespace), "imagePullSecrets": []map[string]string{ {"name": "stackrox"}, }, diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 5f696ecf..44be4e78 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -22,11 +22,68 @@ import ( ) var ( + sharedNamespace = "stackrox" centralNamespace = "acs-central" sensorNamespace = "acs-sensor" defaultExposure = "loadbalancer" pauseReconcileAnnotationKey = "stackrox.io/pause-reconcile" + + allInstallableCentralResourceKinds = []string{ + "applications", + "clusterroles", + "configmaps", + "deployments", + "destinationrules", + "endpoints", + "endpointslices", + "horizontalpodautoscalers", + "networkpolicys", + "leases", + "persistentvolumes", + "persistentvolumeclaims", + "pods", + "podsecuritypolicys", + "prometheusrules", + "roles", + "rolebindings", + "replicasets", + "routes", + "secrets", + "services", + "serviceaccounts", + "servicemonitors", + "storageclasses", + } + + allInstallableSecuredClusterResourceKinds = []string{ + "clusterroles", + "clusterrolebindings", + "configmaps", + "controllerrevisions", + "daemonsets", + "deployments", + "endpoints", + "endpointslices", + "destinationrules", + "horizontalpodautoscalers", + "networkpolicys", + "leases", + "persistentvolumes", + "persistentvolumeclaims", + "pods", + "podsecuritypolicys", + "prometheusrules", + "replicasets", + "roles", + "rolebindings", + "secrets", + "services", + "serviceaccounts", + "servicemonitors", + "storageclasses", + "validatingwebhookconfigurations", + } ) // Deployer is the base deployer for ACS @@ -59,6 +116,159 @@ type Deployer struct { verbose bool earlyReadiness bool dockerCreds *dockerauth.Credentials + clusterResourceKinds map[string]struct{} +} + +type ResourceKindWithName struct { + Kind string + Name string +} + +func (d *Deployer) filterResourceKinds(resourceKinds []string) []string { + filteredResourceKinds := make([]string, 0, len(resourceKinds)) + for _, resourceKind := range resourceKinds { + if _, ok := d.clusterResourceKinds[resourceKind]; ok { + filteredResourceKinds = append(filteredResourceKinds, resourceKind) + } + } + return filteredResourceKinds +} + +func (d *Deployer) deleteResource(ctx context.Context, namespace, resourceType, resourceName string, args ...string) error { + finalArgs := []string{ + "-n", namespace, + "delete", + resourceType, + resourceName, + "--ignore-not-found", + "--force", + "--grace-period=0", + } + finalArgs = append(finalArgs, args...) + _, err := d.runKubectl(ctx, KubectlOptions{Args: finalArgs}) + return err +} + +func (d *Deployer) deleteResources(ctx context.Context, namespace string, resourceTypes []string, args ...string) error { + resourceTypesString := strings.Join(resourceTypes, ",") + finalArgs := []string{ + "-n", namespace, + "delete", + resourceTypesString, + "--ignore-not-found", + "--force", + "--grace-period=0", + } + finalArgs = append(finalArgs, args...) + _, err := d.runKubectl(ctx, KubectlOptions{Args: finalArgs}) + return err +} + +func (d *Deployer) deleteFinalizers(ctx context.Context, namespace, resourceType, resourceName string) error { + _, err := d.runKubectl(ctx, KubectlOptions{ + Args: []string{ + "-n", namespace, "patch", resourceType, resourceName, + "-p", "{\"metadata\":{\"finalizers\":null}}", + "--type=merge", + }, + }) + return err +} + +func (d *Deployer) deleteCentralResources(ctx context.Context, wait bool) error { + d.logger.Info("Deleting Central resources") + var crExists bool + + if d.doesResourceExist(ctx, "central", "stackrox-central-services", d.centralNamespace) { + crExists = true + + // Trigger async deletion of the Central CR. + err := d.deleteResource(ctx, d.centralNamespace, "central", "stackrox-central-services", "--wait=false") + if err != nil { + return fmt.Errorf("failed to asynchronously delete Central CR: %w", err) + } + + err = d.deleteFinalizers(ctx, d.centralNamespace, "central", "stackrox-central-services") + if err != nil { + return fmt.Errorf("failed to delete finalizers on Central CR: %w", err) + } + + } + + // In the meantime, delete other resources by brute force. + resourceKinds := d.filterResourceKinds(allInstallableCentralResourceKinds) + err := d.deleteResources(ctx, d.centralNamespace, resourceKinds, "-l=app.kubernetes.io/part-of=stackrox-central-services") + if err != nil { + return err + } + + for _, resource := range []ResourceKindWithName{ + {Name: "central-db", Kind: "pvc"}, + {Name: "central-db-backup", Kind: "pvc"}, + {Name: "admin-password", Kind: "secret"}, + } { + err := d.deleteResource(ctx, d.centralNamespace, resource.Kind, resource.Name) + if err != nil { + return fmt.Errorf("failed to delete %s/%s: %w", resource.Kind, resource.Name, err) + } + } + + if crExists { + // Now delete the Central CR synchronously. + err := d.deleteResource(ctx, d.centralNamespace, "central", "stackrox-central-services") + if err != nil { + return fmt.Errorf("failed to delete Central CR: %w", err) + } + } + + return nil +} + +func (d *Deployer) deleteSecuredClusterResources(ctx context.Context, wait bool) error { + d.logger.Info("Deleting SecuredCluster resources") + var crExists bool + + if d.doesResourceExist(ctx, "securedcluster", "stackrox-secured-cluster-services", d.sensorNamespace) { + crExists = true + + // Trigger async deletion of the SecuredCluster CR. + err := d.deleteResource(ctx, d.sensorNamespace, "securedcluster", "stackrox-secured-cluster-services", "--wait=false") + if err != nil { + return err + } + + err = d.deleteFinalizers(ctx, d.sensorNamespace, "securedcluster", "stackrox-secured-cluster-services") + if err != nil { + return fmt.Errorf("failed to delete finalizers on SecuredCluster CR: %w", err) + } + } + + // In the meantime, delete other resources by brute force. + resourceKinds := d.filterResourceKinds(allInstallableSecuredClusterResourceKinds) + err := d.deleteResources(ctx, d.sensorNamespace, resourceKinds, "-l=app.kubernetes.io/part-of=stackrox-secured-cluster-services") + if err != nil { + return err + } + + for _, resource := range []ResourceKindWithName{ + {Name: "cluster-registration-secret", Kind: "secret"}, + {Name: "scanner-db-password", Kind: "secret"}, + } { + err := d.deleteResource(ctx, d.sensorNamespace, resource.Kind, resource.Name) + if err != nil { + return fmt.Errorf("failed to delete %s/%s: %w", resource.Kind, resource.Name, err) + } + } + + if crExists { + // Now delete the SecuredCluster CR synchronously. + err := d.deleteResource(ctx, d.sensorNamespace, "securedcluster", "stackrox-secured-cluster-services") + if err != nil { + return fmt.Errorf("failed to delete SecuredCluster CR: %w", err) + } + } + + return nil } func New(log *logger.Logger, overrideFile string, overrideSetExpressions []string) (*Deployer, error) { @@ -114,12 +324,39 @@ func New(log *logger.Logger, overrideFile string, overrideSetExpressions []strin } d.kubeContext = ctx + clusterResourceKinds, err := d.getClusterResourceKinds() + if err != nil { + return nil, fmt.Errorf("failed to get cluster resource kinds: %w", err) + } + d.clusterResourceKinds = clusterResourceKinds + log.Success("🚀 ACS Deployer initialized") log.Infof("roxctl version: %s", d.roxctlVersion) return d, nil } +func (d *Deployer) getClusterResourceKinds() (map[string]struct{}, error) { + result, err := d.runKubectl(context.Background(), KubectlOptions{ + Args: []string{"api-resources", "-o", "name"}, + }) + if err != nil { + return nil, fmt.Errorf("failed to get cluster resource kinds: %w", err) + } + kinds := make(map[string]struct{}) + lines := strings.Split(strings.TrimSpace(result.Stdout), "\n") + for _, line := range lines { + fields := strings.SplitN(line, ".", 2) + if len(fields) == 0 || fields[0] == "" { + continue + } + kind := fields[0] + kinds[kind] = struct{}{} + } + + return kinds, nil +} + func formatComponentName(component string) string { switch component { case "both", "all": @@ -187,14 +424,12 @@ 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.waitForNamespaceDeletion(d.centralNamespace); err != nil { - return fmt.Errorf("failed waiting for namespace deletion: %w", err) - } } portForwardWanted := d.portForwardEnabled @@ -221,14 +456,12 @@ 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.waitForNamespaceDeletion(d.sensorNamespace); err != nil { - return fmt.Errorf("failed waiting for namespace deletion: %w", err) - } } if d.useHelm { @@ -265,30 +498,20 @@ func (d *Deployer) teardownCentral(ctx context.Context) error { d.portForward.Stop() - // Remove pause-reconcile annotation before deleting to allow operator to clean up properly - d.removePauseReconcileAnnotation(ctx, "central", "stackrox-central-services", d.centralNamespace) - - d.logger.Info("Deleting Central custom resource") - d.runKubectl(ctx, KubectlOptions{ - Args: []string{"delete", "central", "stackrox-central-services", "-n", d.centralNamespace, "--wait=false"}, - IgnoreErrors: true, - }) - - time.Sleep(2 * time.Second) - - _, err := d.runKubectl(ctx, KubectlOptions{ - Args: []string{"delete", "namespace", d.centralNamespace, "--wait=false"}, - }) - if err != nil { - return fmt.Errorf("failed to delete namespace: %w", err) + // Add pause-reconcile annotation to not have the operator interfere during resource deletion. + if d.doesResourceExist(ctx, "central", "stackrox-central-services", d.centralNamespace) { + if err := d.addPauseReconcileAnnotation(ctx, "central", "stackrox-central-services", d.centralNamespace); err != nil { + d.logger.Warningf("Error adding pause-reconcile annotation: %v", err) + } } - d.logger.Info("⏳ Waiting for namespace to be fully deleted...") - if err := d.waitForNamespaceDeletion(d.centralNamespace); err != nil { - return fmt.Errorf("failed waiting for namespace deletion: %w", err) + d.logger.Info("⏳ Waiting for Central resources to be fully deleted...") + err := d.deleteCentralResources(ctx, true) + if err != nil { + return fmt.Errorf("failed to delete Central resources: %w", err) } - d.logger.Successf("✓ %s has been deleted", d.centralNamespace) + d.logger.Successf("✓ Central resources in namespace %s have been deleted", d.centralNamespace) return nil } @@ -300,41 +523,24 @@ func (d *Deployer) teardownSecuredCluster(ctx context.Context) error { return nil } - // Remove pause-reconcile annotation before deleting to allow operator to clean up properly - d.removePauseReconcileAnnotation(ctx, "securedcluster", "stackrox-secured-cluster-services", d.sensorNamespace) - - d.logger.Info("Deleting SecuredCluster custom resource") - d.runKubectl(ctx, KubectlOptions{ - Args: []string{"delete", "securedcluster", "stackrox-secured-cluster-services", "-n", d.sensorNamespace, "--wait=false"}, - IgnoreErrors: true, - }) - - time.Sleep(2 * time.Second) - - _, err := d.runKubectl(ctx, KubectlOptions{ - Args: []string{"delete", "namespace", d.sensorNamespace, "--wait=false"}, - }) - if err != nil { - return fmt.Errorf("failed to delete namespace: %w", err) + if d.doesResourceExist(ctx, "securedcluster", "stackrox-secured-cluster-services", d.sensorNamespace) { + // Add pause-reconcile annotation to not have the operator interfere during resource deletion. + if err := d.addPauseReconcileAnnotation(ctx, "securedcluster", "stackrox-secured-cluster-services", d.sensorNamespace); err != nil { + d.logger.Warningf("Error adding pause-reconcile annotation: %v", err) + } } - d.logger.Info("⏳ Waiting for namespace to be fully deleted...") - if err := d.waitForNamespaceDeletion(d.sensorNamespace); err != nil { - return fmt.Errorf("failed waiting for namespace deletion: %w", err) + d.logger.Info("⏳ Waiting for SecuredCluster resources to be fully deleted...") + err := d.deleteSecuredClusterResources(ctx, true) + if err != nil { + return fmt.Errorf("failed to delete SecuredCluster resources: %w", err) } - d.logger.Successf("✓ %s has been deleted", d.sensorNamespace) + d.logger.Successf("✓ SecuredCluster resources in namespace %s have been deleted", d.sensorNamespace) return nil } func (d *Deployer) ensureNamespaceExists(namespace string) error { - if d.isNamespaceTerminating(namespace) { - d.logger.Infof("Namespace %s is terminating, waiting for deletion to complete...", namespace) - if err := d.waitForNamespaceDeletion(namespace); err != nil { - return fmt.Errorf("failed waiting for namespace deletion: %w", err) - } - } - if d.namespaceExists(namespace) { return nil } @@ -358,16 +564,6 @@ func (d *Deployer) namespaceExists(namespace string) bool { return err == nil } -func (d *Deployer) isNamespaceTerminating(namespace string) bool { - result, err := d.runKubectl(context.Background(), KubectlOptions{ - Args: []string{"get", "namespace", namespace, "-o", "jsonpath={.status.phase}"}, - }) - if err != nil { - return false - } - return strings.TrimSpace(result.Stdout) == "Terminating" -} - func (d *Deployer) waitForNamespaceDeletion(namespace string) error { timeout := 5 * time.Minute checkInterval := 2 * time.Second @@ -515,6 +711,13 @@ func (d *Deployer) SetPauseReconciliation(enabled bool) { d.pauseReconciliation = enabled } +func (d *Deployer) SetSingleNamespace(enabled bool) { + if enabled { + d.centralNamespace = sharedNamespace + d.sensorNamespace = sharedNamespace + } +} + // maybeAddPauseReconcileAnnotation adds the stackrox.io/pause-reconcile annotation to a custom resource func (d *Deployer) maybeAddPauseReconcileAnnotation(ctx context.Context, resourceType, resourceName, namespace string) error { if !d.pauseReconciliation { @@ -523,36 +726,39 @@ func (d *Deployer) maybeAddPauseReconcileAnnotation(ctx context.Context, resourc d.logger.Infof("Adding pause-reconcile annotation to %s/%s", resourceType, resourceName) - _, err := d.runKubectl(ctx, KubectlOptions{ - Args: []string{ - "annotate", resourceType, resourceName, - "-n", namespace, - pauseReconcileAnnotationKey, - "--overwrite", - }, - }) + err := d.addPauseReconcileAnnotation(ctx, resourceType, resourceName, namespace) if err != nil { - return fmt.Errorf("failed to add pause-reconcile annotation: %w", err) + return err } d.logger.Successf("✓ Added pause-reconcile annotation to %s/%s", resourceType, resourceName) return nil } -// removePauseReconcileAnnotation removes the stackrox.io/pause-reconcile annotation from a custom resource -func (d *Deployer) removePauseReconcileAnnotation(ctx context.Context, resourceType, resourceName, namespace string) { - d.logger.Dimf("Removing pause-reconcile annotation from %s/%s", resourceType, resourceName) +func (d *Deployer) doesResourceExist(ctx context.Context, resourceType, resourceName, namespace string) bool { + _, err := d.runKubectl(ctx, KubectlOptions{ + Args: []string{ + "get", resourceType, resourceName, + "-n", namespace, + }, + }) + return err == nil +} +func (d *Deployer) addPauseReconcileAnnotation(ctx context.Context, resourceType, resourceName, namespace string) error { _, err := d.runKubectl(ctx, KubectlOptions{ Args: []string{ "annotate", resourceType, resourceName, "-n", namespace, - fmt.Sprintf("%s-", pauseReconcileAnnotationKey), + fmt.Sprintf("%s=%s", pauseReconcileAnnotationKey, "true"), + "--overwrite", }, }) if err != nil { - d.logger.Dimf("Could not remove pause-reconcile annotation (expected if CR does not exist): %v", err) + return fmt.Errorf("failed to add pause-reconcile annotation: %w", err) } + + return nil } func (d *Deployer) SetDeployOperator(deployOperator bool) { diff --git a/tests/e2e/basic_test.go b/tests/e2e/basic_test.go index b087fb62..4724821b 100644 --- a/tests/e2e/basic_test.go +++ b/tests/e2e/basic_test.go @@ -25,7 +25,7 @@ func TestDeployBothSimple(t *testing.T) { defer os.Remove(envrcPath) t.Log("=== Deploying both components together ===") - args := append([]string{roxieBinary, "deploy", "both", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) + args := append([]string{roxieBinary, "deploy", "--early-readiness", "both", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) runCommand(t, deployTimeout*2, nil, args...) // Verify namespaces exist @@ -43,8 +43,7 @@ func TestDeployBothSimple(t *testing.T) { teardownArgs := []string{roxieBinary, "teardown", "both"} runCommand(t, teardownTimeout, nil, teardownArgs...) - // Verify namespaces are deleted - t.Log("Verifying namespaces are removed") - verifyNamespaceAbsent(t, "acs-central") - verifyNamespaceAbsent(t, "acs-sensor") + t.Log("Verifying components are removed") + verifyCentralNotInstalled(t, "acs-central") + verifySecuredClusterNotInstalled(t, "acs-sensor") } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index a5c75c54..c4a8887a 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -57,10 +57,44 @@ func TestMain(m *testing.M) { } fmt.Printf("Using roxie binary: %s\n", roxieBinary) + // Teardown all deployments before running tests + if err := teardownAllDeployments(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: teardown all deployments failed: %v\n", err) + os.Exit(1) + } + // Run tests os.Exit(m.Run()) } +func teardownAllDeployments() error { + fmt.Println("=== Tearing down all deployments before running tests ===") + + ctx, cancel := context.WithTimeout(context.Background(), teardownTimeout) + defer cancel() + + // Teardown standard deployments + cmd := exec.CommandContext(ctx, roxieBinary, "teardown") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("Warning: teardown command failed: %w", err) + } + + // Teardown single-namespace deployments + ctx, cancel = context.WithTimeout(context.Background(), teardownTimeout) + defer cancel() + cmd = exec.CommandContext(ctx, roxieBinary, "teardown", "--single-namespace") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("Warning: teardown --single-namespace command failed: %w", err) + } + + fmt.Println("=== All deployments have been torn down ===") + return nil +} + func requireBinary(name string) error { _, err := exec.LookPath(name) return err @@ -162,29 +196,15 @@ func verifyNamespaceExists(t *testing.T, namespace string) { } } -func verifyNamespaceAbsent(t *testing.T, namespace string) { +func doesDeploymentExist(t *testing.T, namespace string, name string) bool { t.Helper() - // Wait up to 5 minutes for namespace to be deleted - // Kubernetes namespace deletion can take time, especially with finalizers - timeout := time.After(5 * time.Minute) - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-timeout: - t.Fatalf("Timeout waiting for namespace %s to be deleted", namespace) - case <-ticker.C: - cmd := exec.Command("kubectl", "get", "namespace", namespace) - err := cmd.Run() - if err != nil { - // Namespace is gone - success! - return - } - // Still exists, keep waiting - } + cmd := exec.Command("kubectl", "-n", namespace, "get", "deployments", "-o", "name") + output, err := cmd.Output() + if err != nil { + t.Fatalf("Failed to get deployments in namespace %s: %v", namespace, err) } + return strings.Contains(string(output), name) } func loadEnvrcFile(path string) (map[string]string, error) { @@ -238,7 +258,7 @@ func TestDeployCentralAndSecuredCluster(t *testing.T) { // Deploy central t.Log("=== Deploying central ===") - args := append([]string{roxieBinary, "deploy", "central", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) + args := append([]string{roxieBinary, "deploy", "--early-readiness", "central", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) runCommand(t, deployTimeout, nil, args...) // Load environment from envrc file for secured-cluster deployment @@ -249,7 +269,7 @@ func TestDeployCentralAndSecuredCluster(t *testing.T) { t.Log("Loaded environment from envrc file for secured-cluster") t.Log("=== Deploying secured-cluster ===") - args = append([]string{roxieBinary, "deploy", "secured-cluster"}, commonDeployArgsNoPortForward...) + args = append([]string{roxieBinary, "deploy", "--early-readiness", "secured-cluster"}, commonDeployArgsNoPortForward...) runCommand(t, deployTimeout, envrcEnv, args...) // Verify namespaces @@ -272,10 +292,41 @@ func TestTeardownCentralAndSecuredCluster(t *testing.T) { args := []string{roxieBinary, "teardown", "both"} runCommand(t, teardownTimeout, nil, args...) - // Verify namespaces are deleted - t.Log("Verifying namespaces are removed") - verifyNamespaceAbsent(t, "acs-central") - verifyNamespaceAbsent(t, "acs-sensor") + t.Log("Verifying components are removed") + verifyCentralNotInstalled(t, "acs-central") + verifySecuredClusterNotInstalled(t, "acs-sensor") +} + +func verifyCentralInstalled(t *testing.T, namespace string) { + t.Helper() + + if !doesDeploymentExist(t, namespace, "central") { + t.Fatalf("Central is not installed in namespace %s", namespace) + } +} + +func verifySecuredClusterInstalled(t *testing.T, namespace string) { + t.Helper() + + if !doesDeploymentExist(t, namespace, "sensor") { + t.Fatalf("Secured cluster is not installed in namespace %s", namespace) + } +} + +func verifyCentralNotInstalled(t *testing.T, namespace string) { + t.Helper() + + if doesDeploymentExist(t, namespace, "central") { + t.Fatalf("Central is installed in namespace %s", namespace) + } +} + +func verifySecuredClusterNotInstalled(t *testing.T, namespace string) { + t.Helper() + + if doesDeploymentExist(t, namespace, "sensor") { + t.Fatalf("Secured cluster is installed in namespace %s", namespace) + } } func TestDeployBothComponentsTogether(t *testing.T) { @@ -313,6 +364,35 @@ func TestDeployBothComponentsTogether(t *testing.T) { } +func TestDeployBothComponentsTogetherInSingleNamespace(t *testing.T) { + if os.Getenv("SKIP_OPERATOR_TESTS") != "" { + t.Skip("SKIP_OPERATOR_TESTS is set") + } + + // Create temporary envrc file. + envrcFile, err := os.CreateTemp("", ".envrc.roxie-test-*") + if err != nil { + t.Fatalf("Failed to create temp envrc: %v", err) + } + envrcPath := envrcFile.Name() + envrcFile.Close() + defer os.Remove(envrcPath) + + t.Log("=== Deploying both components in single namespace ===") + args := append([]string{roxieBinary, "deploy", "both", "--single-namespace", "--early-readiness", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) + runCommand(t, deployTimeout*2, nil, args...) + + verifyCentralInstalled(t, "stackrox") + verifySecuredClusterInstalled(t, "stackrox") + + t.Log("=== Tearing down both components in single namespace ===") + args = []string{roxieBinary, "teardown", "--single-namespace"} + runCommand(t, teardownTimeout, nil, args...) + + verifyCentralNotInstalled(t, "stackrox") + verifySecuredClusterNotInstalled(t, "stackrox") +} + func TestDeployCentralAndSecuredClusterViaHelm(t *testing.T) { // Create temporary envrc file envrcFile, err := os.CreateTemp("", ".envrc.roxie-test-*") @@ -324,7 +404,7 @@ func TestDeployCentralAndSecuredClusterViaHelm(t *testing.T) { defer os.Remove(envrcPath) t.Log("=== Deploying central via Helm ===") - args := append([]string{roxieBinary, "deploy", "central", "--helm", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) + args := append([]string{roxieBinary, "deploy", "--early-readiness", "central", "--helm", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) runCommand(t, deployTimeout*2, nil, args...) // Load environment from envrc file for secured-cluster deployment @@ -335,7 +415,7 @@ func TestDeployCentralAndSecuredClusterViaHelm(t *testing.T) { t.Log("Loaded environment from envrc file for secured-cluster") t.Log("=== Deploying secured-cluster via Helm ===") - args = append([]string{roxieBinary, "deploy", "secured-cluster", "--helm"}, commonDeployArgsNoPortForward...) + args = append([]string{roxieBinary, "deploy", "--early-readiness", "secured-cluster", "--helm"}, commonDeployArgsNoPortForward...) runCommand(t, deployTimeout*2, envrcEnv, args...) t.Log("Verifying namespace: acs-central") diff --git a/tests/e2e/olm_switch_test.go b/tests/e2e/olm_switch_test.go index 155f0f38..90fae0fa 100644 --- a/tests/e2e/olm_switch_test.go +++ b/tests/e2e/olm_switch_test.go @@ -113,7 +113,7 @@ func TestOLMToNonOLMSwitch(t *testing.T) { teardownArgs := []string{roxieBinary, "teardown", "central"} runCommand(t, teardownTimeout, nil, teardownArgs...) - verifyNamespaceAbsent(t, "acs-central") + verifyCentralNotInstalled(t, "acs-central") } // TestNonOLMToOLMSwitch tests switching from non-OLM operator to OLM operator @@ -165,7 +165,7 @@ func TestNonOLMToOLMSwitch(t *testing.T) { teardownArgs := []string{roxieBinary, "teardown", "central"} runCommand(t, teardownTimeout, nil, teardownArgs...) - verifyNamespaceAbsent(t, "acs-central") + verifyCentralNotInstalled(t, "acs-central") } // TestOLMOperatorVersionUpgrade tests that OLM operator version mismatches trigger teardown and redeploy @@ -231,7 +231,7 @@ func TestOLMOperatorVersionUpgrade(t *testing.T) { teardownArgs := []string{roxieBinary, "teardown", "central"} runCommand(t, teardownTimeout, nil, teardownArgs...) - verifyNamespaceAbsent(t, "acs-central") + verifyCentralNotInstalled(t, "acs-central") } // TestSecuredClusterWithOLMSwitch tests that secured-cluster deployment also respects OLM mode switches @@ -251,7 +251,7 @@ func TestSecuredClusterWithOLMSwitch(t *testing.T) { // Step 1: Deploy central with OLM t.Log("=== Step 1: Deploy central with OLM ===") - args := append([]string{roxieBinary, "deploy", "central", "--olm", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) + args := append([]string{roxieBinary, "deploy", "--early-readiness", "central", "--olm", "--envrc", envrcPath}, commonDeployArgsNoPortForward...) runCommand(t, deployTimeout, nil, args...) verifyOperatorMode(t, true) @@ -265,7 +265,7 @@ func TestSecuredClusterWithOLMSwitch(t *testing.T) { // Step 2: Deploy secured-cluster (should reuse OLM operator) t.Log("=== Step 2: Deploy secured-cluster (should reuse OLM operator) ===") - args = append([]string{roxieBinary, "deploy", "secured-cluster", "--olm"}, commonDeployArgsNoPortForward...) + args = append([]string{roxieBinary, "deploy", "--early-readiness", "secured-cluster", "--olm"}, commonDeployArgsNoPortForward...) runCommand(t, deployTimeout, envrcEnv, args...) // Verify operator is still in OLM mode @@ -274,7 +274,7 @@ func TestSecuredClusterWithOLMSwitch(t *testing.T) { // Step 3: Switch to non-OLM by redeploying secured-cluster without --olm t.Log("=== Step 3: Redeploy secured-cluster without OLM (triggering mode switch) ===") - args = append([]string{roxieBinary, "deploy", "secured-cluster"}, commonDeployArgsNoPortForward...) + args = append([]string{roxieBinary, "deploy", "--early-readiness", "secured-cluster"}, commonDeployArgsNoPortForward...) runCommand(t, deployTimeout, envrcEnv, args...) // Verify operator switched to non-OLM mode @@ -286,6 +286,6 @@ func TestSecuredClusterWithOLMSwitch(t *testing.T) { teardownArgs := []string{roxieBinary, "teardown", "both"} runCommand(t, teardownTimeout, nil, teardownArgs...) - verifyNamespaceAbsent(t, "acs-central") - verifyNamespaceAbsent(t, "acs-sensor") + verifyCentralNotInstalled(t, "acs-central") + verifySecuredClusterNotInstalled(t, "acs-sensor") }