diff --git a/config/config.go b/config/config.go index 35a5e1e8..8ea493c3 100644 --- a/config/config.go +++ b/config/config.go @@ -113,11 +113,21 @@ func setVersion() (string, error) { return "", fmt.Errorf("failed get cluster version: %w", err) } - r := regexp.MustCompile(`Server Version: ([1-9]\.[0-9]+)\..*`) + // Try to match Server Version first (preferred) + r := regexp.MustCompile(`Server Version: ([0-9]+\.[0-9]+)`) matches := r.FindSubmatch(rawversion) - if len(matches) < 2 { - return "", fmt.Errorf("couldn't get server version from output: %s", rawversion) + if len(matches) >= 2 { + return string(matches[1]), nil } - return string(matches[1]), nil + + // Fallback to Kubernetes Version if Server Version not found + r2 := regexp.MustCompile(`Kubernetes Version: v([0-9]+\.[0-9]+)`) + matches = r2.FindSubmatch(rawversion) + + if len(matches) >= 2 { + return string(matches[1]), nil + } + + return "", fmt.Errorf("couldn't get server version from output: %s", rawversion) } diff --git a/e2e_test.go b/e2e_test.go index c19f9f75..eeb6a084 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -6,6 +6,7 @@ import ( "log" "os" "path" + "strings" "testing" "time" @@ -497,3 +498,149 @@ func TestProfileRemediations(t *testing.T) { t.Logf("Warning: Failed to wait for scan cleanup for binding %s: %s", bindingName, err) } } + +// TestRosaNodeProfilesWithoutPlatformEnv tests that the Compliance Operator works +// correctly on ROSA HCP clusters when installed WITHOUT the PLATFORM environment +// variable set in the subscription. +func TestRosaNodeProfilesWithoutPlatformEnv(t *testing.T) { + // Skip if not running on ROSA platform + if tc.Platform != "rosa" { + t.Skipf("Skipping ROSA-specific test: -platform is %s", tc.Platform) + } + + c, err := helpers.GenerateKubeConfig() + if err != nil { + t.Fatalf("Failed to generate kube config: %s", err) + } + + bindingName := "rosa-node-profiles-test" + + // Cleanup function + defer func() { + t.Log("Cleaning up test resources") + err := helpers.DeleteScanBinding(tc, c, bindingName) + if err != nil { + t.Logf("Warning: Failed to delete scan binding: %s", err) + } + err = helpers.WaitForScanCleanup(tc, c, bindingName) + if err != nil { + t.Logf("Warning: Failed to wait for scan cleanup: %s", err) + } + }() + + // Step 1: Verify subscription does NOT have PLATFORM env variable + t.Log("Verifying subscription has no PLATFORM environment variable set") + hasPlatformEnv, err := helpers.SubscriptionHasPlatformEnv(tc, c) + if err != nil { + t.Fatalf("Failed to check subscription config: %s", err) + } + if hasPlatformEnv { + t.Fatal("Subscription has PLATFORM env variable set, but it should not be set for this test") + } + t.Log("✓ Subscription has no PLATFORM env variable (as expected)") + + // Step 2: Verify ProfileBundles are VALID + t.Log("Verifying ProfileBundles are VALID") + err = helpers.VerifyProfileBundleStatus(tc, c, "ocp4", "VALID") + if err != nil { + t.Fatalf("Failed to verify ocp4 ProfileBundle: %s", err) + } + err = helpers.VerifyProfileBundleStatus(tc, c, "rhcos4", "VALID") + if err != nil { + t.Fatalf("Failed to verify rhcos4 ProfileBundle: %s", err) + } + t.Log("✓ ProfileBundles (ocp4, rhcos4) are VALID") + + // Step 3: Verify node profiles exist + t.Log("Verifying node profiles are available") + nodeProfiles := []string{ + "ocp4-cis-node", + "ocp4-pci-dss-node", + "ocp4-high-node", + "ocp4-moderate-node", + "ocp4-nerc-cip-node", + "ocp4-stig-node", + "rhcos4-e8", + "rhcos4-high", + "rhcos4-moderate", + "rhcos4-nerc-cip", + "rhcos4-stig", + } + for _, profileName := range nodeProfiles { + err := helpers.ValidateProfile(tc, c, profileName) + if err != nil { + t.Fatalf("Expected node profile %s to exist, but got error: %s", profileName, err) + } + } + t.Logf("Verified %d node profiles exist", len(nodeProfiles)) + + // Step 4: Verify platform profile ocp4-cis does NOT exist + t.Log("Verifying platform profile ocp4-cis does NOT exist") + err = helpers.ValidateProfile(tc, c, "ocp4-cis") + if err == nil { + t.Fatal("Platform profile ocp4-cis should NOT exist on ROSA without PLATFORM env, but it was found") + } + t.Log("Platform profile ocp4-cis does not exist (as expected)") + + // Step 5: Create ScanSettingBinding with two node profiles + t.Log("Creating ScanSettingBinding with ocp4-cis-node and ocp4-pci-dss-node profiles") + err = helpers.CreateRosaNodeScanBinding(tc, c, bindingName, "ocp4-cis-node", "ocp4-pci-dss-node") + if err != nil { + t.Fatalf("Failed to create scan binding: %s", err) + } + t.Log("Created ScanSettingBinding") + + // Step 6: Wait for ComplianceSuite to complete + t.Log("Waiting for ComplianceSuite to complete") + err = helpers.WaitForComplianceSuite(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to wait for compliance suite: %s", err) + } + t.Log("ComplianceSuite completed") + + // Step 7: Get scan results + t.Log("Retrieving scan results") + results, err := helpers.CreateResultMap(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to create result map: %s", err) + } + + // Save results for debugging + err = helpers.SaveResultAsYAML(tc, results, "rosa-node-profiles-test-results.yaml") + if err != nil { + t.Logf("Warning: Failed to save test results: %s", err) + } + + // Step 8: Verify we got scan results + // We expect NON-COMPLIANT results on a fresh cluster, but the main point + // is that the scan completed successfully + if len(results) == 0 { + t.Fatal("No scan results found - scan may not have executed properly") + } + t.Logf("Scan completed with %d check results", len(results)) + + // Verify that at least some checks were from the node profiles + foundCisNodeCheck := false + foundPciNodeCheck := false + for checkName := range results { + if strings.Contains(checkName, "ocp4-cis-node") { + foundCisNodeCheck = true + } + if strings.Contains(checkName, "ocp4-pci-dss-node") { + foundPciNodeCheck = true + } + } + + if !foundCisNodeCheck { + t.Error("Expected to find check results from ocp4-cis-node profile") + } + if !foundPciNodeCheck { + t.Error("Expected to find check results from ocp4-pci-dss-node profile") + } + + if !foundCisNodeCheck || !foundPciNodeCheck { + t.Fatal("Not all expected profiles were scanned") + } + + t.Log("ROSA node profile test passed successfully when Compliance Operator installed without PLATFORM env variable") +} diff --git a/helpers/utilities.go b/helpers/utilities.go index 4a8f5afe..8ce0f213 100644 --- a/helpers/utilities.go +++ b/helpers/utilities.go @@ -29,6 +29,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" k8syaml "k8s.io/apimachinery/pkg/util/yaml" @@ -1903,3 +1904,148 @@ func convertMarkdownToHTML(markdown string) string { `, html) } + +// SubscriptionHasPlatformEnv checks if the Compliance Operator subscription +// has the PLATFORM environment variable configured in .spec.config.env +func SubscriptionHasPlatformEnv(tc *testConfig.TestConfig, c dynclient.Client) (bool, error) { + sub := &unstructured.Unstructured{} + sub.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "operators.coreos.com", + Version: "v1alpha1", + Kind: "Subscription", + }) + + // Try common subscription names (web console uses "compliance-operator", test framework uses "compliance-operator-sub") + subscriptionNames := []string{"compliance-operator", "compliance-operator-sub"} + var lastErr error + + for _, name := range subscriptionNames { + err := c.Get(goctx.TODO(), dynclient.ObjectKey{ + Name: name, + Namespace: tc.OperatorNamespace.Namespace, + }, sub) + if err == nil { + break // Found the subscription + } + lastErr = err + } + + if lastErr != nil && sub.GetName() == "" { + return false, fmt.Errorf("failed to get subscription (tried: %v): %w", subscriptionNames, lastErr) + } + + // Check if .spec.config.env exists + envVars, found, err := unstructured.NestedSlice(sub.Object, "spec", "config", "env") + if err != nil { + return false, fmt.Errorf("failed to get env from subscription: %w", err) + } + + if !found || envVars == nil || len(envVars) == 0 { + // No env variables configured + return false, nil + } + + // Check if any env variable has name="PLATFORM" + for _, envVar := range envVars { + envMap, ok := envVar.(map[string]interface{}) + if !ok { + continue + } + name, found, err := unstructured.NestedString(envMap, "name") + if err != nil { + continue + } + if found && name == "PLATFORM" { + return true, nil + } + } + + return false, nil +} + +// VerifyProfileBundleStatus checks that a ProfileBundle exists and has the expected status +func VerifyProfileBundleStatus(tc *testConfig.TestConfig, c dynclient.Client, bundleName, expectedStatus string) error { + pb := &cmpv1alpha1.ProfileBundle{} + err := c.Get(goctx.TODO(), dynclient.ObjectKey{ + Name: bundleName, + Namespace: tc.OperatorNamespace.Namespace, + }, pb) + if err != nil { + return fmt.Errorf("failed to get ProfileBundle %s: %w", bundleName, err) + } + + if string(pb.Status.DataStreamStatus) != expectedStatus { + return fmt.Errorf("ProfileBundle %s has status %s, expected %s", + bundleName, pb.Status.DataStreamStatus, expectedStatus) + } + + log.Printf("ProfileBundle %s has status %s", bundleName, expectedStatus) + return nil +} + +// CreateRosaNodeScanBinding creates a ScanSettingBinding for ROSA node profile testing +// with two specified node profiles +func CreateRosaNodeScanBinding(tc *testConfig.TestConfig, c dynclient.Client, + bindingName, profile1, profile2 string) error { + binding := &cmpv1alpha1.ScanSettingBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: bindingName, + Namespace: tc.OperatorNamespace.Namespace, + }, + SettingsRef: &cmpv1alpha1.NamedObjectReference{ + APIGroup: "compliance.openshift.io/v1alpha1", + Kind: "ScanSetting", + Name: "default", + }, + Profiles: []cmpv1alpha1.NamedObjectReference{ + { + APIGroup: "compliance.openshift.io/v1alpha1", + Kind: "Profile", + Name: profile1, + }, + { + APIGroup: "compliance.openshift.io/v1alpha1", + Kind: "Profile", + Name: profile2, + }, + }, + } + + // Check if the binding already exists and delete it if it does + existingBinding := &cmpv1alpha1.ScanSettingBinding{} + err := c.Get(goctx.TODO(), dynclient.ObjectKey{ + Name: bindingName, + Namespace: tc.OperatorNamespace.Namespace, + }, existingBinding) + + if err == nil { + // Binding exists, delete it first + log.Printf("Deleting existing ScanSettingBinding %s to trigger new scan\n", bindingName) + err = c.Delete(goctx.TODO(), existingBinding) + if err != nil { + return fmt.Errorf("failed to delete existing %s scan binding: %w", bindingName, err) + } + + // Wait for ComplianceSuite and ComplianceCheckResults to be cleaned up + err = waitForScanCleanup(c, tc, bindingName) + if err != nil { + return fmt.Errorf("failed to wait for scan cleanup after deleting %s: %w", bindingName, err) + } + } else if !apierrors.IsNotFound(err) { + // If error is not "not found", return the error + return fmt.Errorf("failed to check if %s scan binding exists: %w", bindingName, err) + } + + // Create the new binding + bo := backoff.WithMaxRetries(backoff.NewConstantBackOff(tc.APIPollInterval), 180) + err = backoff.RetryNotify(func() error { + return c.Create(goctx.TODO(), binding) + }, bo, func(err error, d time.Duration) { + fmt.Printf("Couldn't create %s binding after %s: %s\n", bindingName, d.String(), err) + }) + if err != nil { + return fmt.Errorf("failed to create %s scan binding: %w", bindingName, err) + } + log.Printf("Created new ScanSettingBinding %s with profiles: %s, %s\n", bindingName, profile1, profile2) + return nil +}