Skip to content

Commit 669e039

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 669e039

7 files changed

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

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)