diff --git a/e2e_test.go b/e2e_test.go index c19f9f75..90bbbdbc 100644 --- a/e2e_test.go +++ b/e2e_test.go @@ -497,3 +497,119 @@ func TestProfileRemediations(t *testing.T) { t.Logf("Warning: Failed to wait for scan cleanup for binding %s: %s", bindingName, err) } } + +// TestNamespaceExemptionVariables tests the namespace exemption logic for +// resource limit checks. This test validates that: +// 1. Workloads without resource limits in exempted namespaces pass the check +// 2. The exemption variables work correctly for DaemonSet, Deployment, and StatefulSet +func TestNamespaceExemptionVariables(t *testing.T) { + // Skip if test type doesn't include platform tests + if tc.TestType != "platform" && tc.TestType != "all" { + t.Skipf("Skipping namespace exemption test: -test-type is %s", tc.TestType) + } + + c, err := helpers.GenerateKubeConfig() + if err != nil { + t.Fatalf("Failed to generate kube config: %s", err) + } + + // Test namespace names + testNamespaces := []string{ + "ns-76797-test-1", + "ns-76797-test-2", + } + + // Create test namespaces + for _, ns := range testNamespaces { + err = createNamespace(c, ns) + if err != nil { + t.Fatalf("Failed to create test namespace %s: %s", ns, err) + } + t.Logf("Created test namespace: %s", ns) + } + + // Cleanup namespaces at the end + defer func() { + for _, ns := range testNamespaces { + deleteNamespace(c, ns) + } + }() + + // Create workloads without resource limits in test namespaces + err = createTestWorkloadsWithoutLimits(c, testNamespaces[0]) + if err != nil { + t.Fatalf("Failed to create test workloads: %s", err) + } + t.Logf("Created test workloads without resource limits in %s", testNamespaces[0]) + + // Wait for workloads to be created + time.Sleep(5 * time.Second) + + // Build regex pattern to exempt test namespaces + // Pattern matches both test namespaces + exemptionPattern := "^ns-76797-test-.*$" + + // Create TailoredProfile with namespace exemption variables + tailoredProfileName := "ns-exemption-test-profile" + err = createTailoredProfileWithExemptions(tc, c, tailoredProfileName, exemptionPattern) + if err != nil { + t.Fatalf("Failed to create tailored profile with exemptions: %s", err) + } + t.Logf("Created TailoredProfile: %s with exemption pattern: %s", tailoredProfileName, exemptionPattern) + + // Create scan binding for the tailored profile + bindingName := "ns-exemption-scan-binding" + err = helpers.CreateScanBinding(c, tc, bindingName, tailoredProfileName, "TailoredProfile", "default") + if err != nil { + t.Fatalf("Failed to create scan binding: %s", err) + } + t.Logf("Created ScanSettingBinding: %s", bindingName) + + // Wait for compliance suite to complete + err = helpers.WaitForComplianceSuite(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to wait for compliance suite: %s", err) + } + + // Get scan results + results, err := helpers.CreateResultMap(tc, c, bindingName) + if err != nil { + t.Fatalf("Failed to create result map: %s", err) + } + + // Verify that resource limit rules PASS because namespaces are exempted + expectedRules := map[string]string{ + "resource-requests-limits-in-daemonset": "PASS", + "resource-requests-limits-in-deployment": "PASS", + "resource-requests-limits-in-statefulset": "PASS", + } + + var failures []string + for ruleName, expectedResult := range expectedRules { + // Find the actual result - the result name might include scan name prefix + actualResult := findRuleResult(results, ruleName) + if actualResult == "" { + failures = append(failures, fmt.Sprintf("Rule %s not found in scan results", ruleName)) + continue + } + + if actualResult != expectedResult { + failures = append(failures, + fmt.Sprintf("Rule %s: expected %s, got %s", ruleName, expectedResult, actualResult)) + } else { + t.Logf("Rule %s: %s (namespace exemption working correctly)", ruleName, actualResult) + } + } + + // Save results for debugging + err = helpers.SaveResultAsYAML(tc, results, "namespace-exemption-test-results.yaml") + if err != nil { + t.Logf("Warning: Failed to save test results: %s", err) + } + + if len(failures) > 0 { + t.Fatalf("Namespace exemption test failed:\n%v", failures) + } + + t.Log("Namespace exemption test passed successfully - all exempted workloads passed the resource limit checks") +} diff --git a/helpers.go b/helpers.go index ce42406d..e44dd14e 100644 --- a/helpers.go +++ b/helpers.go @@ -1,5 +1,17 @@ package ocp4e2e +import ( + "context" + "strings" + + cmpv1alpha1 "github.com/ComplianceAsCode/compliance-operator/pkg/apis/compliance/v1alpha1" + "github.com/ComplianceAsCode/ocp4e2e/config" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + dynclient "sigs.k8s.io/controller-runtime/pkg/client" +) + // RuleTest is the definition of the structure rule-specific e2e tests should have. type RuleTest struct { DefaultResult interface{} `yaml:"default_result"` @@ -13,3 +25,114 @@ type RuleTestResults struct { func init() { } + +// createTailoredProfileWithExemptions creates a TailoredProfile for namespace exemption testing. +func createTailoredProfileWithExemptions(tc *config.TestConfig, c dynclient.Client, name, exemptionPattern string) error { + tp := &cmpv1alpha1.TailoredProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, Namespace: tc.OperatorNamespace.Namespace, + Annotations: map[string]string{"compliance.openshift.io/product-type": "Platform"}, + }, + Spec: cmpv1alpha1.TailoredProfileSpec{ + Title: "Namespace Exemption Test Profile", + Description: "Test profile for validating namespace exemption variables", + EnableRules: []cmpv1alpha1.RuleReferenceSpec{ + {Name: "ocp4-resource-requests-limits-in-daemonset"}, + {Name: "ocp4-resource-requests-limits-in-deployment"}, + {Name: "ocp4-resource-requests-limits-in-statefulset"}, + }, + SetValues: []cmpv1alpha1.VariableValueSpec{ + {Name: "ocp4-var-daemonset-limit-namespaces-exempt-regex", Value: exemptionPattern}, + {Name: "ocp4-var-deployment-limit-namespaces-exempt-regex", Value: exemptionPattern}, + {Name: "ocp4-var-statefulset-limit-namespaces-exempt-regex", Value: exemptionPattern}, + }, + }, + } + return c.Create(context.TODO(), tp) +} + +// createTestWorkloadsWithoutLimits creates test workloads without resource limits. +func createTestWorkloadsWithoutLimits(c dynclient.Client, namespace string) error { + ctx := context.TODO() + + workloads := []dynclient.Object{ + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "test-deployment-no-limits", Namespace: namespace}, + Spec: appsv1.DeploymentSpec{ + Replicas: int32Ptr(1), + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{ + Name: "nginx", Image: "registry.access.redhat.com/ubi8/ubi-minimal:latest", + Command: []string{"/bin/sh", "-c", "sleep infinity"}, + }}}, + }, + }, + }, + &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Name: "test-daemonset-no-limits", Namespace: namespace}, + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "nginx", Image: "registry.access.redhat.com/ubi8/ubi-minimal:latest", + Command: []string{"/bin/sh", "-c", "sleep infinity"}, + }}, + Tolerations: []corev1.Toleration{{Operator: corev1.TolerationOpExists}}, + }, + }, + }, + }, + &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{Name: "test-statefulset-no-limits", Namespace: namespace}, + Spec: appsv1.StatefulSetSpec{ + Replicas: int32Ptr(1), ServiceName: "test", + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{ + Name: "nginx", Image: "registry.access.redhat.com/ubi8/ubi-minimal:latest", + Command: []string{"/bin/sh", "-c", "sleep infinity"}, + }}}, + }, + }, + }, + } + + for _, w := range workloads { + if err := c.Create(ctx, w); err != nil { + return err + } + } + return nil +} + +// createNamespace creates a namespace. +func createNamespace(c dynclient.Client, name string) error { + return c.Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}) +} + +// deleteNamespace deletes a namespace. +func deleteNamespace(c dynclient.Client, name string) error { + c.Delete(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}) + return nil +} + +// findRuleResult searches for a rule result by partial name match. +func findRuleResult(results map[string]string, ruleName string) string { + if result, exists := results[ruleName]; exists { + return result + } + for resultName, resultValue := range results { + if strings.Contains(resultName, ruleName) { + return resultValue + } + } + return "" +} + +// int32Ptr returns a pointer to an int32 value. +func int32Ptr(i int32) *int32 { return &i }