Skip to content

Commit db231a2

Browse files
Vincent056claude
andcommitted
CMP-4475: Make e2e-profile usable for custom content locally + fix log dir
- Add CONTENT_IMAGE and LOG_DIR make variables (passed as -content-image / -log-dir) so e2e-profile can test a custom content image (e.g. one built with CEL content) and write artifacts to a writable directory off-CI. - Create the log directory before writing assertion/mismatch/report artifacts (GenerateAssertionFileFromResults, SaveMismatchesAsYAML, GenerateMismatchReport) so runs don't fail when the dir does not already exist. - Add opt-in TestCELProfileBundleLive validating the typed CELContentFile path. Verified end to end on a fresh OCP 4.21 cluster: make e2e-profile installs CNV, creates the CEL ProfileBundle (VALID), scans the cis-vm-extension profile, and records results. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c4dbe5e commit db231a2

3 files changed

Lines changed: 110 additions & 2 deletions

File tree

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ INSTALL_VIRT?=false
1616
# Path (inside the content image) to the CEL content YAML. Set for CEL profiles
1717
# (e.g. the CIS OpenShift Virtualization benchmark), e.g. ocp4-cel-content.yaml.
1818
CEL_CONTENT_FILE?=
19+
# Content image to test. Leave empty to use the built-in default; set to a
20+
# custom image (e.g. one built with CEL content) to test it.
21+
CONTENT_IMAGE?=
22+
CONTENT_IMAGE_FLAG:=$(if $(strip $(CONTENT_IMAGE)),-content-image="$(CONTENT_IMAGE)")
23+
# Directory for logs/artifacts (assertion files, reports). Defaults to the CI
24+
# path; override for local runs, e.g. LOG_DIR=/tmp/artifacts.
25+
LOG_DIR?=
26+
LOG_DIR_FLAG:=$(if $(strip $(LOG_DIR)),-log-dir="$(LOG_DIR)")
1927

2028
GOLANGCI_LINT_VERSION=latest
2129
BUILD_DIR := build
@@ -37,11 +45,11 @@ e2e-node: install-jq ## Run only node compliance tests
3745

3846
.PHONY: e2e-profile
3947
e2e-profile: install-jq ## Run TestProfile test only
40-
set -o pipefail; PATH=$$PATH:/tmp/bin go test $(TEST_FLAGS) . -run=^TestProfile$$ -profile="$(PROFILE)" -product="$(PRODUCT)" -install-operator=$(INSTALL_OPERATOR) -install-virt=$(INSTALL_VIRT) -cel-content-file="$(CEL_CONTENT_FILE)" | tee .e2e-profile-test-results.out
48+
set -o pipefail; PATH=$$PATH:/tmp/bin go test $(TEST_FLAGS) . -run=^TestProfile$$ -profile="$(PROFILE)" -product="$(PRODUCT)" -install-operator=$(INSTALL_OPERATOR) -install-virt=$(INSTALL_VIRT) -cel-content-file="$(CEL_CONTENT_FILE)" $(CONTENT_IMAGE_FLAG) $(LOG_DIR_FLAG) | tee .e2e-profile-test-results.out
4149

4250
.PHONY: e2e-profile-remediations
4351
e2e-profile-remediations: install-jq ## Run TestProfile test only
44-
set -o pipefail; PATH=$$PATH:/tmp/bin go test $(TEST_FLAGS) . -run=^TestProfileRemediations$$ -profile="$(PROFILE)" -product="$(PRODUCT)" -install-operator=$(INSTALL_OPERATOR) -install-virt=$(INSTALL_VIRT) -cel-content-file="$(CEL_CONTENT_FILE)" | tee .e2e-profile-test-results.out
52+
set -o pipefail; PATH=$$PATH:/tmp/bin go test $(TEST_FLAGS) . -run=^TestProfileRemediations$$ -profile="$(PROFILE)" -product="$(PRODUCT)" -install-operator=$(INSTALL_OPERATOR) -install-virt=$(INSTALL_VIRT) -cel-content-file="$(CEL_CONTENT_FILE)" $(CONTENT_IMAGE_FLAG) $(LOG_DIR_FLAG) | tee .e2e-profile-test-results.out
4553

4654
.PHONY: help
4755
help: ## Show this help screen

helpers/celbundle_live_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package helpers
2+
3+
import (
4+
goctx "context"
5+
"os"
6+
"testing"
7+
"time"
8+
9+
cmpv1alpha1 "github.com/ComplianceAsCode/compliance-operator/pkg/apis/compliance/v1alpha1"
10+
backoff "github.com/cenkalti/backoff/v4"
11+
apierrors "k8s.io/apimachinery/pkg/api/errors"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/types"
14+
dynclient "sigs.k8s.io/controller-runtime/pkg/client"
15+
)
16+
17+
// TestCELProfileBundleLive creates a ProfileBundle with the typed
18+
// spec.celContentFile field (v1.9.0 API) against a real cluster and verifies it
19+
// parses VALID. Opt-in: RUN_CEL_PB_LIVE=1, CONTENT_IMAGE=<image>,
20+
// CEL_CONTENT_FILE=<file>. Validates the dependency bump end to end.
21+
func TestCELProfileBundleLive(t *testing.T) {
22+
if os.Getenv("RUN_CEL_PB_LIVE") != "1" {
23+
t.Skip("set RUN_CEL_PB_LIVE=1 to run")
24+
}
25+
img := os.Getenv("CONTENT_IMAGE")
26+
cel := os.Getenv("CEL_CONTENT_FILE")
27+
if img == "" || cel == "" {
28+
t.Fatal("CONTENT_IMAGE and CEL_CONTENT_FILE must be set")
29+
}
30+
c, err := GenerateKubeConfig()
31+
if err != nil {
32+
t.Fatalf("client: %s", err)
33+
}
34+
ns := "openshift-compliance"
35+
name := "ocp4-virt-typed"
36+
37+
pb := &cmpv1alpha1.ProfileBundle{
38+
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns},
39+
Spec: cmpv1alpha1.ProfileBundleSpec{
40+
ContentImage: img,
41+
ContentFile: "ssg-ocp4-ds.xml",
42+
CELContentFile: cel, // <-- the field added in v1.9.0
43+
},
44+
}
45+
_ = c.Delete(goctx.TODO(), pb)
46+
if err := c.Create(goctx.TODO(), pb); err != nil && !apierrors.IsAlreadyExists(err) {
47+
t.Fatalf("create ProfileBundle: %s", err)
48+
}
49+
t.Logf("Created ProfileBundle %s with celContentFile=%s", name, cel)
50+
51+
// Wait until the bundle parses VALID.
52+
key := types.NamespacedName{Name: name, Namespace: ns}
53+
bo := backoff.WithMaxRetries(backoff.NewConstantBackOff(10*time.Second), 30)
54+
if err := backoff.RetryNotify(func() error {
55+
found := &cmpv1alpha1.ProfileBundle{}
56+
if err := c.Get(goctx.TODO(), key, found); err != nil {
57+
return err
58+
}
59+
if found.Status.DataStreamStatus != cmpv1alpha1.DataStreamValid {
60+
return &statusErr{string(found.Status.DataStreamStatus)}
61+
}
62+
// confirm the field round-tripped through the typed API
63+
if found.Spec.CELContentFile != cel {
64+
t.Fatalf("celContentFile not persisted: got %q", found.Spec.CELContentFile)
65+
}
66+
return nil
67+
}, bo, func(err error, d time.Duration) {
68+
t.Logf("waiting for ProfileBundle VALID after %s: %s", d, err)
69+
}); err != nil {
70+
t.Fatalf("ProfileBundle did not become VALID: %s", err)
71+
}
72+
t.Logf("ProfileBundle %s is VALID and celContentFile round-tripped via typed API", name)
73+
74+
// Confirm the CEL profile was created from the CEL content file.
75+
profKey := types.NamespacedName{Name: name + "-cis-vm-extension", Namespace: ns}
76+
prof := &cmpv1alpha1.Profile{}
77+
if err := c.Get(goctx.TODO(), profKey, prof); err != nil {
78+
t.Fatalf("expected CEL profile %s: %s", profKey.Name, err)
79+
}
80+
scanner := prof.Annotations["compliance.openshift.io/scanner-type"]
81+
t.Logf("CEL profile %s created (scanner-type=%s) with %d rules", profKey.Name, scanner, len(prof.Rules))
82+
if scanner != "CEL" {
83+
t.Fatalf("expected scanner-type CEL, got %q", scanner)
84+
}
85+
}
86+
87+
type statusErr struct{ s string }
88+
89+
func (e *statusErr) Error() string { return "ProfileBundle status: " + e.s }
90+
91+
var _ dynclient.Object = &cmpv1alpha1.ProfileBundle{}

helpers/utilities.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,9 @@ func GenerateAssertionFileFromResults(
10101010
return fmt.Errorf("failed to marshal assertion content: %w", err)
10111011
}
10121012

1013+
if err := os.MkdirAll(tc.LogDir, 0o755); err != nil {
1014+
return fmt.Errorf("failed to create log directory %s: %w", tc.LogDir, err)
1015+
}
10131016
fullPath := path.Join(tc.LogDir, assertionFile)
10141017
err = os.WriteFile(fullPath, data, 0o600)
10151018
if err != nil {
@@ -1790,6 +1793,9 @@ func SaveMismatchesAsYAML(tc *testConfig.TestConfig, mismatchedAssertions []Asse
17901793
if err != nil {
17911794
return fmt.Errorf("failed to marshal results to YAML: %w", err)
17921795
}
1796+
if err := os.MkdirAll(tc.LogDir, 0o755); err != nil {
1797+
return fmt.Errorf("failed to create log directory %s: %w", tc.LogDir, err)
1798+
}
17931799
err = os.WriteFile(p, yamlData, 0o600)
17941800
if err != nil {
17951801
return fmt.Errorf("failed to write YAML file: %w", err)
@@ -1848,6 +1854,9 @@ func GenerateMismatchReport(
18481854
f := fmt.Sprintf("%s-report.md", bindingName)
18491855
p := path.Join(tc.LogDir, f)
18501856

1857+
if err := os.MkdirAll(tc.LogDir, 0o755); err != nil {
1858+
return fmt.Errorf("failed to create log directory %s: %w", tc.LogDir, err)
1859+
}
18511860
err := os.WriteFile(p, []byte(report.String()), 0o600)
18521861
if err != nil {
18531862
return fmt.Errorf("failed to write markdown report: %w", err)

0 commit comments

Comments
 (0)