Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
147 changes: 147 additions & 0 deletions e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"os"
"path"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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")
}
146 changes: 146 additions & 0 deletions helpers/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1903,3 +1904,148 @@ func convertMarkdownToHTML(markdown string) string {
</body>
</html>`, 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
}