Skip to content

Commit e0d9dd0

Browse files
committed
Add componentNames field to VolatileCriteria
Add optional componentNames field to allow filtering volatile config include/exclude rules by component name from ApplicationSnapshot. Example usage: ``` volatileConfig: exclude: - value: "lint.has_failures" componentNames: ["component1", "component2"] ``` Ref: https://issues.redhat.com/browse/EC-1513 Assisted-by: Cursor (using claude-4.5-sonnet)
1 parent e1d2a37 commit e0d9dd0

7 files changed

Lines changed: 201 additions & 2 deletions

api/config/appstudio.redhat.com_enterprisecontractpolicies.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ spec:
159159
items:
160160
description: VolatileCriteria includes or excludes a policy rule with effective dates as an option.
161161
properties:
162+
componentNames:
163+
description: |-
164+
ComponentNames is used to specify component names from
165+
ApplicationSnapshot. This allows filtering in scenarios where
166+
multiple components share the same image repository.
167+
items:
168+
type: string
169+
minLength: 1
170+
minItems: 1
171+
type: array
172+
x-kubernetes-list-type: set
162173
effectiveOn:
163174
format: date-time
164175
type: string
@@ -195,6 +206,17 @@ spec:
195206
items:
196207
description: VolatileCriteria includes or excludes a policy rule with effective dates as an option.
197208
properties:
209+
componentNames:
210+
description: |-
211+
ComponentNames is used to specify component names from
212+
ApplicationSnapshot. This allows filtering in scenarios where
213+
multiple components share the same image repository.
214+
items:
215+
type: string
216+
minLength: 1
217+
minItems: 1
218+
type: array
219+
x-kubernetes-list-type: set
198220
effectiveOn:
199221
format: date-time
200222
type: string

api/v1alpha1/enterprisecontractpolicy_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ type VolatileCriteria struct {
116116
// +kubebuilder:validation:Pattern=`^[a-z0-9][a-z0-9.-]*[a-z0-9](?:\/[a-z0-9][a-z0-9-]*[a-z0-9]){2,}$`
117117
ImageUrl string `json:"imageUrl,omitempty"`
118118

119+
// ComponentNames is used to specify component names from
120+
// ApplicationSnapshot. This allows filtering in scenarios where
121+
// multiple components share the same image repository.
122+
// +optional
123+
// +kubebuilder:validation:MinItems:=1
124+
// +kubebuilder:validation:items:MinLength:=1
125+
// +listType:=set
126+
ComponentNames []string `json:"componentNames,omitempty"`
127+
119128
// Reference is used to include a link to related information such as a Jira issue URL.
120129
// +optional
121130
Reference string `json:"reference,omitempty"`

api/v1alpha1/enterprisecontractpolicy_types_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,130 @@ func TestReferenceField(t *testing.T) {
396396
})
397397
}
398398
}
399+
400+
func TestComponentNamesField(t *testing.T) {
401+
tests := []struct {
402+
name string
403+
componentNames []string
404+
wantValid bool
405+
omitField bool // true if the field should be omitted entirely
406+
}{
407+
// Valid cases
408+
{"Single component", []string{"component1"}, true, false},
409+
{"Multiple components", []string{"component1", "component2", "component3"}, true, false},
410+
{"Component with hyphens", []string{"my-component"}, true, false},
411+
{"Component with numbers", []string{"component123"}, true, false},
412+
{"Omitted field", nil, true, true}, // Field is omitted entirely
413+
{"Duplicate components", []string{"c1", "c1"}, true, false}, // CRD allows duplicates (listType=set not enforced)
414+
415+
// Invalid cases
416+
{"Empty array", []string{}, false, false}, // Violates MinItems:=1
417+
{"Empty string", []string{""}, false, false}, // Violates items:MinLength:=1
418+
}
419+
420+
for _, tt := range tests {
421+
t.Run(tt.name, func(t *testing.T) {
422+
// Create a policy with the test component names
423+
policy := EnterpriseContractPolicy{
424+
Spec: EnterpriseContractPolicySpec{
425+
Sources: []Source{
426+
{
427+
VolatileConfig: &VolatileSourceConfig{
428+
Exclude: []VolatileCriteria{
429+
{
430+
Value: "test-rule",
431+
},
432+
},
433+
},
434+
},
435+
},
436+
},
437+
}
438+
if !tt.omitField {
439+
policy.Spec.Sources[0].VolatileConfig.Exclude[0].ComponentNames = tt.componentNames
440+
}
441+
442+
// Create a CRD validation schema
443+
crd := v1.CustomResourceDefinition{}
444+
bytes, err := os.ReadFile("../../config/crd/bases/appstudio.redhat.com_enterprisecontractpolicies.yaml")
445+
if err != nil {
446+
t.Fatalf("unexpected error reading CRD: %s", err)
447+
}
448+
if err := yaml.Unmarshal(bytes, &crd); err != nil {
449+
t.Fatalf("unexpected error when decoding schema: %s", err)
450+
}
451+
452+
crdv := apiextensions.CustomResourceValidation{}
453+
if err := v1.Convert_v1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(crd.Spec.Versions[0].Schema, &crdv, nil); err != nil {
454+
t.Fatalf("failed in CRD validation conversion: %s", err)
455+
}
456+
457+
s, err := schema.NewStructural(crdv.OpenAPIV3Schema)
458+
if err != nil {
459+
t.Fatalf("unexpected error when creating structural: %s", err)
460+
}
461+
462+
v := validation.NewSchemaValidatorFromOpenAPI(s.ToKubeOpenAPI())
463+
464+
// Convert policy to unstructured for validation
465+
obj := unstructured.Unstructured{}
466+
obj.SetUnstructuredContent(map[string]interface{}{
467+
"apiVersion": "appstudio.redhat.com/v1alpha1",
468+
"kind": "EnterpriseContractPolicy",
469+
"spec": map[string]interface{}{
470+
"sources": []interface{}{
471+
map[string]interface{}{
472+
"volatileConfig": map[string]interface{}{
473+
"exclude": []interface{}{
474+
func() map[string]interface{} {
475+
m := map[string]interface{}{
476+
"value": "test-rule",
477+
}
478+
if !tt.omitField {
479+
// Convert []string to []interface{}
480+
componentNamesInterface := make([]interface{}, len(tt.componentNames))
481+
for i, name := range tt.componentNames {
482+
componentNamesInterface[i] = name
483+
}
484+
m["componentNames"] = componentNamesInterface
485+
}
486+
return m
487+
}(),
488+
},
489+
},
490+
},
491+
},
492+
},
493+
})
494+
495+
// Validate the object
496+
result := v.Validate(&obj)
497+
isValid := result.IsValid()
498+
499+
if isValid != tt.wantValid {
500+
t.Errorf("Validation for %v = %v, want %v. Errors: %v", tt.componentNames, isValid, tt.wantValid, result.Errors)
501+
}
502+
503+
// Also validate the actual policy object
504+
// Note: Empty arrays with omitempty are omitted during JSON marshaling,
505+
// so they're treated as omitted fields and pass validation
506+
if !tt.omitField && len(tt.componentNames) > 0 && tt.wantValid {
507+
policyObj := unstructured.Unstructured{}
508+
policyBytes, err := json.Marshal(policy)
509+
if err != nil {
510+
t.Fatalf("unexpected error marshaling policy: %s", err)
511+
}
512+
if err := json.Unmarshal(policyBytes, &policyObj.Object); err != nil {
513+
t.Fatalf("unexpected error unmarshaling policy: %s", err)
514+
}
515+
516+
policyResult := v.Validate(&policyObj)
517+
policyIsValid := policyResult.IsValid()
518+
519+
if policyIsValid != tt.wantValid {
520+
t.Errorf("Policy validation for %v = %v, want %v. Errors: %v", tt.componentNames, policyIsValid, tt.wantValid, policyResult.Errors)
521+
}
522+
}
523+
})
524+
}
525+
}

api/v1alpha1/policy_spec.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@
180180
"type": "string",
181181
"description": "ImageUrl is used to specify an image by its URL without a tag.\n+optional\n+kubebuilder:validation:Pattern=`^[a-z0-9][a-z0-9.-]*[a-z0-9](?:\\/[a-z0-9][a-z0-9-]*[a-z0-9]){2,}$`"
182182
},
183+
"componentNames": {
184+
"items": {
185+
"type": "string"
186+
},
187+
"type": "array",
188+
"description": "ComponentNames is used to specify component names from\nApplicationSnapshot. This allows filtering in scenarios where\nmultiple components share the same image repository.\n+optional\n+kubebuilder:validation:MinItems:=1\n+kubebuilder:validation:items:MinLength:=1\n+listType:=set"
189+
},
183190
"reference": {
184191
"type": "string",
185192
"description": "Reference is used to include a link to related information such as a Jira issue URL.\n+optional"

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/appstudio.redhat.com_enterprisecontractpolicies.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ spec:
159159
items:
160160
description: VolatileCriteria includes or excludes a policy rule with effective dates as an option.
161161
properties:
162+
componentNames:
163+
description: |-
164+
ComponentNames is used to specify component names from
165+
ApplicationSnapshot. This allows filtering in scenarios where
166+
multiple components share the same image repository.
167+
items:
168+
type: string
169+
minLength: 1
170+
minItems: 1
171+
type: array
172+
x-kubernetes-list-type: set
162173
effectiveOn:
163174
format: date-time
164175
type: string
@@ -195,6 +206,17 @@ spec:
195206
items:
196207
description: VolatileCriteria includes or excludes a policy rule with effective dates as an option.
197208
properties:
209+
componentNames:
210+
description: |-
211+
ComponentNames is used to specify component names from
212+
ApplicationSnapshot. This allows filtering in scenarios where
213+
multiple components share the same image repository.
214+
items:
215+
type: string
216+
minLength: 1
217+
minItems: 1
218+
type: array
219+
x-kubernetes-list-type: set
198220
effectiveOn:
199221
format: date-time
200222
type: string

docs/modules/ROOT/pages/reference.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ Appears In: xref:{anchor_prefix}-github-com-enterprise-contract-enterprise-contr
205205
ImageRef is used to specify an image by its digest. +
206206
| *`imageDigest`* __string__ | ImageDigest is used to specify an image by its digest. +
207207
| *`imageUrl`* __string__ | ImageUrl is used to specify an image by its URL without a tag. +
208+
| *`componentNames`* __string array__ | ComponentNames is used to specify component names from +
209+
ApplicationSnapshot. This allows filtering in scenarios where +
210+
multiple components share the same image repository. +
208211
| *`reference`* __string__ | Reference is used to include a link to related information such as a Jira issue URL. +
209212
|===
210213

0 commit comments

Comments
 (0)