Skip to content

Commit b410dce

Browse files
committed
Add componentNames filter for volatileConfig criteria
Add optional componentNames field to allow filtering volatile config include/exclude rules by component name from ApplicationSnapshot. This allows filtering in scenarios where multiple components share the same image repository. Example usage: ``` volatileConfig: exclude: - value: "lint.has_failures" componentNames: ["component1", "component2"] ``` **Depends on: github.com/conforma/crds with ComponentNames field Ref: https://issues.redhat.com/browse/EC-1513 Assisted-by: Cursor (using claude-4.5-sonnet)
1 parent 1a6ca15 commit b410dce

File tree

9 files changed

+946
-67
lines changed

9 files changed

+946
-67
lines changed

internal/evaluator/conftest_evaluator.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
646646

647647
// Filter results using the unified filter
648648
filteredResults, updatedMissingIncludes := unifiedFilter.FilterResults(
649-
allResults, allRules, target.Target, missingIncludes, effectiveTime)
649+
allResults, allRules, target.Target, target.ComponentName, missingIncludes, effectiveTime)
650650

651651
// Update missing includes
652652
missingIncludes = updatedMissingIncludes
@@ -661,7 +661,7 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
661661
result.Skipped = skipped
662662

663663
// Replace the placeholder successes slice with the actual successes.
664-
result.Successes = c.computeSuccesses(result, rules, target.Target, missingIncludes, unifiedFilter)
664+
result.Successes = c.computeSuccesses(result, rules, target.Target, target.ComponentName, missingIncludes, unifiedFilter)
665665

666666
totalRules += len(result.Warnings) + len(result.Failures) + len(result.Successes)
667667

@@ -798,7 +798,8 @@ func toRules(results []output.Result) []Result {
798798
func (c conftestEvaluator) computeSuccesses(
799799
result Outcome,
800800
rules policyRules,
801-
target string,
801+
imageRef string,
802+
componentName string,
802803
missingIncludes map[string]bool,
803804
unifiedFilter PostEvaluationFilter,
804805
) []Result {
@@ -857,15 +858,15 @@ func (c conftestEvaluator) computeSuccesses(
857858
if unifiedFilter != nil {
858859
// Use the unified filter to check if this success should be included
859860
filteredResults, _ := unifiedFilter.FilterResults(
860-
[]Result{success}, rules, target, missingIncludes, time.Now())
861+
[]Result{success}, rules, imageRef, componentName, missingIncludes, time.Now())
861862

862863
if len(filteredResults) == 0 {
863864
log.Debugf("Skipping result success: %#v", success)
864865
continue
865866
}
866867
} else {
867868
// Fallback to legacy filtering for backward compatibility
868-
if !c.isResultIncluded(success, target, missingIncludes) {
869+
if !c.isResultIncluded(success, imageRef, componentName, missingIncludes) {
869870
log.Debugf("Skipping result success: %#v", success)
870871
continue
871872
}
@@ -1120,10 +1121,10 @@ func isResultEffective(failure Result, now time.Time) bool {
11201121
// isResultIncluded returns whether or not the result should be included or
11211122
// discarded based on the policy configuration.
11221123
// 'missingIncludes' is a list of include directives that gets pruned if the result is matched
1123-
func (c conftestEvaluator) isResultIncluded(result Result, target string, missingIncludes map[string]bool) bool {
1124+
func (c conftestEvaluator) isResultIncluded(result Result, imageRef string, componentName string, missingIncludes map[string]bool) bool {
11241125
ruleMatchers := LegacyMakeMatchers(result)
1125-
includeScore := LegacyScoreMatches(ruleMatchers, c.include.get(target), missingIncludes)
1126-
excludeScore := LegacyScoreMatches(ruleMatchers, c.exclude.get(target), map[string]bool{})
1126+
includeScore := LegacyScoreMatches(ruleMatchers, c.include.get(imageRef, componentName), missingIncludes)
1127+
excludeScore := LegacyScoreMatches(ruleMatchers, c.exclude.get(imageRef, componentName), map[string]bool{})
11271128
return includeScore > excludeScore
11281129
}
11291130

internal/evaluator/conftest_evaluator_integration_basic_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,204 @@ deny contains result if {
139139
require.NoError(t, err)
140140
assert.NotNil(t, result)
141141
}
142+
143+
func TestConftestEvaluatorIntegrationWithComponentNames(t *testing.T) {
144+
ctx := context.Background()
145+
146+
// Create a temporary directory for the test
147+
tmpDir := t.TempDir()
148+
policyDir := filepath.Join(tmpDir, "policy")
149+
err := os.MkdirAll(policyDir, 0o755)
150+
require.NoError(t, err)
151+
152+
// Create policies that will be filtered by ComponentNames
153+
policyContent := `package test
154+
155+
import rego.v1
156+
157+
# METADATA
158+
# title: Check A
159+
# custom:
160+
# short_name: check_a
161+
deny contains result if {
162+
result := {
163+
"code": "test.check_a",
164+
"msg": "Check A always fails"
165+
}
166+
}
167+
168+
# METADATA
169+
# title: Check B
170+
# custom:
171+
# short_name: check_b
172+
deny contains result if {
173+
result := {
174+
"code": "test.check_b",
175+
"msg": "Check B always fails"
176+
}
177+
}
178+
`
179+
err = os.WriteFile(filepath.Join(policyDir, "policy.rego"), []byte(policyContent), 0o600)
180+
require.NoError(t, err)
181+
182+
// Create policy source
183+
policySource := &source.PolicyUrl{
184+
Url: "file://" + policyDir,
185+
Kind: source.PolicyKind,
186+
}
187+
188+
// Create config provider with ComponentNames filter
189+
configProvider := &mockConfigProvider{}
190+
configProvider.On("EffectiveTime").Return(time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC))
191+
configProvider.On("SigstoreOpts").Return(policy.SigstoreOpts{}, nil)
192+
configProvider.On("Spec").Return(ecc.EnterpriseContractPolicySpec{
193+
Sources: []ecc.Source{
194+
{
195+
Policy: []string{"file://" + policyDir},
196+
},
197+
},
198+
})
199+
200+
// Create evaluator with VolatileConfig that excludes check_a for comp1
201+
evaluator, err := NewConftestEvaluator(ctx, []source.PolicySource{policySource}, configProvider, ecc.Source{
202+
VolatileConfig: &ecc.VolatileSourceConfig{
203+
Exclude: []ecc.VolatileCriteria{
204+
{
205+
Value: "test.check_a",
206+
ComponentNames: []string{"comp1"},
207+
EffectiveOn: "2024-01-01T00:00:00Z",
208+
EffectiveUntil: "2025-01-01T00:00:00Z",
209+
},
210+
},
211+
},
212+
})
213+
require.NoError(t, err)
214+
defer evaluator.Destroy()
215+
216+
// Debug: Check exclude criteria
217+
conftestEval := evaluator.(conftestEvaluator)
218+
t.Logf("Exclude componentItems: %+v", conftestEval.exclude.componentItems)
219+
t.Logf("Exclude defaultItems: %+v", conftestEval.exclude.defaultItems)
220+
t.Logf("Exclude digestItems: %+v", conftestEval.exclude.digestItems)
221+
222+
// Create test input
223+
inputData := map[string]interface{}{
224+
"test": "value",
225+
}
226+
inputBytes, err := json.Marshal(inputData)
227+
require.NoError(t, err)
228+
inputPath := filepath.Join(tmpDir, "input.json")
229+
err = os.WriteFile(inputPath, inputBytes, 0o600)
230+
require.NoError(t, err)
231+
232+
// Test comp1 - check_a should be excluded
233+
target1 := EvaluationTarget{
234+
Inputs: []string{inputPath},
235+
Target: "quay.io/repo/img@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
236+
ComponentName: "comp1",
237+
}
238+
239+
result1, err := evaluator.Evaluate(ctx, target1)
240+
require.NoError(t, err)
241+
require.NotNil(t, result1)
242+
243+
// Debug: Print all failures
244+
t.Logf("comp1 results: %d outcomes", len(result1))
245+
for i, outcome := range result1 {
246+
t.Logf(" Outcome %d: %d failures, %d successes", i, len(outcome.Failures), len(outcome.Successes))
247+
for _, failure := range outcome.Failures {
248+
t.Logf(" Failure: %s", failure.Metadata["code"])
249+
}
250+
for _, success := range outcome.Successes {
251+
t.Logf(" Success: %s", success.Metadata["code"])
252+
}
253+
}
254+
255+
// Verify check_a is excluded, check_b is not
256+
hasCheckA := false
257+
hasCheckB := false
258+
for _, outcome := range result1 {
259+
for _, failure := range outcome.Failures {
260+
if codeStr, ok := failure.Metadata["code"].(string); ok {
261+
if codeStr == "test.check_a" {
262+
hasCheckA = true
263+
}
264+
if codeStr == "test.check_b" {
265+
hasCheckB = true
266+
}
267+
}
268+
}
269+
}
270+
assert.False(t, hasCheckA, "Expected check_a to be excluded for comp1")
271+
assert.True(t, hasCheckB, "Expected check_b to be evaluated for comp1")
272+
273+
// Test comp2 - check_a should NOT be excluded
274+
target2 := EvaluationTarget{
275+
Inputs: []string{inputPath},
276+
Target: "quay.io/repo/img@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
277+
ComponentName: "comp2",
278+
}
279+
280+
result2, err := evaluator.Evaluate(ctx, target2)
281+
require.NoError(t, err)
282+
require.NotNil(t, result2)
283+
284+
// Verify both checks are evaluated for comp2
285+
hasCheckA2 := false
286+
hasCheckB2 := false
287+
for _, outcome := range result2 {
288+
for _, failure := range outcome.Failures {
289+
if codeStr, ok := failure.Metadata["code"].(string); ok {
290+
if codeStr == "test.check_a" {
291+
hasCheckA2 = true
292+
}
293+
if codeStr == "test.check_b" {
294+
hasCheckB2 = true
295+
}
296+
}
297+
}
298+
}
299+
assert.True(t, hasCheckA2, "Expected check_a to be evaluated for comp2")
300+
assert.True(t, hasCheckB2, "Expected check_b to be evaluated for comp2")
301+
302+
// Test same image with different components - monorepo scenario
303+
sameImage := "quay.io/monorepo@sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"
304+
305+
target3 := EvaluationTarget{
306+
Inputs: []string{inputPath},
307+
Target: sameImage,
308+
ComponentName: "comp1",
309+
}
310+
311+
result3, err := evaluator.Evaluate(ctx, target3)
312+
require.NoError(t, err)
313+
314+
hasCheckA3 := false
315+
for _, outcome := range result3 {
316+
for _, failure := range outcome.Failures {
317+
if codeStr, ok := failure.Metadata["code"].(string); ok && codeStr == "test.check_a" {
318+
hasCheckA3 = true
319+
}
320+
}
321+
}
322+
assert.False(t, hasCheckA3, "Expected check_a excluded for comp1 even with different image")
323+
324+
target4 := EvaluationTarget{
325+
Inputs: []string{inputPath},
326+
Target: sameImage,
327+
ComponentName: "comp2",
328+
}
329+
330+
result4, err := evaluator.Evaluate(ctx, target4)
331+
require.NoError(t, err)
332+
333+
hasCheckA4 := false
334+
for _, outcome := range result4 {
335+
for _, failure := range outcome.Failures {
336+
if codeStr, ok := failure.Metadata["code"].(string); ok && codeStr == "test.check_a" {
337+
hasCheckA4 = true
338+
}
339+
}
340+
}
341+
assert.True(t, hasCheckA4, "Expected check_a evaluated for comp2 with same image")
342+
}

internal/evaluator/criteria.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,23 @@ import (
2828
// contains include/exclude items
2929
// digestItems stores include/exclude items that are specific with an imageRef
3030
// - the imageRef is the key, value is the policy to include/exclude.
31+
// componentItems stores include/exclude items that are specific to a component name
32+
// - the component name is the key, value is the policy to include/exclude.
3133
// defaultItems are include/exclude items without an imageRef
3234
type Criteria struct {
33-
digestItems map[string][]string
34-
defaultItems []string
35+
digestItems map[string][]string
36+
componentItems map[string][]string
37+
defaultItems []string
3538
}
3639

3740
func (c *Criteria) len() int {
3841
totalLength := len(c.defaultItems)
3942
for _, items := range c.digestItems {
4043
totalLength += len(items)
4144
}
45+
for _, items := range c.componentItems {
46+
totalLength += len(items)
47+
}
4248
return totalLength
4349
}
4450

@@ -64,12 +70,20 @@ func (c *Criteria) addArray(key string, values []string) {
6470
}
6571
}
6672

67-
// This accepts an image ref with digest
68-
// and looks up the image url and digest separately.
69-
func (c *Criteria) get(key string) []string {
70-
ref, err := name.ParseReference(key)
73+
func (c *Criteria) addComponentItem(componentName, value string) {
74+
if c.componentItems == nil {
75+
c.componentItems = make(map[string][]string)
76+
}
77+
c.componentItems[componentName] = append(c.componentItems[componentName], value)
78+
}
79+
80+
// This accepts an image ref with digest and optional component name,
81+
// and looks up the image url, digest, and component name separately.
82+
func (c *Criteria) get(imageRef string, componentName string) []string {
83+
ref, err := name.ParseReference(imageRef)
7184
if err != nil {
72-
log.Debugf("error parsing target image url: %q", key)
85+
log.Debugf("error parsing target image url: %q", imageRef)
86+
// Return only global defaults if image ref is invalid
7387
return c.defaultItems
7488
}
7589

@@ -87,6 +101,13 @@ func (c *Criteria) get(key string) []string {
87101
items = append(items, c.getWithKey(k)...)
88102
}
89103

104+
// Add component-specific items if component name is provided
105+
if componentName != "" {
106+
if componentItems, ok := c.componentItems[componentName]; ok {
107+
items = append(items, componentItems...)
108+
}
109+
}
110+
90111
// Add any exceptions that pertain to all images.
91112
return append(items, c.defaultItems...)
92113
}
@@ -158,6 +179,10 @@ func collectVolatileConfigItems(items *Criteria, volatileCriteria []ecc.Volatile
158179
items.addItem(c.ImageUrl, c.Value)
159180
} else if c.ImageDigest != "" {
160181
items.addItem(c.ImageDigest, c.Value)
182+
} else if len(c.ComponentNames) > 0 {
183+
for _, componentName := range c.ComponentNames {
184+
items.addComponentItem(componentName, c.Value)
185+
}
161186
} else {
162187
items.addItem("", c.Value)
163188
}

0 commit comments

Comments
 (0)