Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions api/config/appstudio.redhat.com_enterprisecontractpolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ spec:
items:
description: VolatileCriteria includes or excludes a policy rule with effective dates as an option.
properties:
componentNames:
description: |-
ComponentNames is used to specify component names from
ApplicationSnapshot. This allows filtering in scenarios where
multiple components share the same image repository.
items:
type: string
minLength: 1
minItems: 1
type: array
x-kubernetes-list-type: set
effectiveOn:
format: date-time
type: string
Expand Down Expand Up @@ -195,6 +206,17 @@ spec:
items:
description: VolatileCriteria includes or excludes a policy rule with effective dates as an option.
properties:
componentNames:
description: |-
ComponentNames is used to specify component names from
ApplicationSnapshot. This allows filtering in scenarios where
multiple components share the same image repository.
items:
type: string
minLength: 1
minItems: 1
type: array
x-kubernetes-list-type: set
effectiveOn:
format: date-time
type: string
Expand Down
9 changes: 9 additions & 0 deletions api/v1alpha1/enterprisecontractpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ type VolatileCriteria struct {
// +kubebuilder:validation:Pattern=`^[a-z0-9][a-z0-9.-]*[a-z0-9](?:\/[a-z0-9][a-z0-9-]*[a-z0-9]){2,}$`
ImageUrl string `json:"imageUrl,omitempty"`

// ComponentNames is used to specify component names from
// ApplicationSnapshot. This allows filtering in scenarios where
// multiple components share the same image repository.
// +optional
// +kubebuilder:validation:MinItems:=1
// +kubebuilder:validation:items:MinLength=1
// +listType:=set
ComponentNames []string `json:"componentNames,omitempty"`

// Reference is used to include a link to related information such as a Jira issue URL.
// +optional
Reference string `json:"reference,omitempty"`
Expand Down
126 changes: 126 additions & 0 deletions api/v1alpha1/enterprisecontractpolicy_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,129 @@ func TestReferenceField(t *testing.T) {
})
}
}

func TestComponentNamesField(t *testing.T) {
tests := []struct {
name string
componentNames []string
wantValid bool
omitField bool // true if the field should be omitted entirely
}{
// Valid cases
{"Single component", []string{"component1"}, true, false},
{"Multiple components", []string{"component1", "component2", "component3"}, true, false},
{"Component with hyphens", []string{"my-component"}, true, false},
{"Component with numbers", []string{"component123"}, true, false},
{"Omitted field", nil, true, true}, // Field is omitted entirely

// Invalid cases
{"Empty array", []string{}, false, false}, // Violates MinItems:=1
{"Empty string", []string{""}, false, false}, // Violates items:MinLength:=1
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a policy with the test component names
policy := EnterpriseContractPolicy{
Spec: EnterpriseContractPolicySpec{
Sources: []Source{
{
VolatileConfig: &VolatileSourceConfig{
Exclude: []VolatileCriteria{
{
Value: "test-rule",
},
},
},
},
},
},
}
if !tt.omitField {
policy.Spec.Sources[0].VolatileConfig.Exclude[0].ComponentNames = tt.componentNames
}

// Create a CRD validation schema
crd := v1.CustomResourceDefinition{}
bytes, err := os.ReadFile("../../config/crd/bases/appstudio.redhat.com_enterprisecontractpolicies.yaml")
if err != nil {
t.Fatalf("unexpected error reading CRD: %s", err)
}
if err := yaml.Unmarshal(bytes, &crd); err != nil {
t.Fatalf("unexpected error when decoding schema: %s", err)
}

crdv := apiextensions.CustomResourceValidation{}
if err := v1.Convert_v1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(crd.Spec.Versions[0].Schema, &crdv, nil); err != nil {
t.Fatalf("failed in CRD validation conversion: %s", err)
}

s, err := schema.NewStructural(crdv.OpenAPIV3Schema)
if err != nil {
t.Fatalf("unexpected error when creating structural: %s", err)
}

v := validation.NewSchemaValidatorFromOpenAPI(s.ToKubeOpenAPI())

// Convert policy to unstructured for validation
obj := unstructured.Unstructured{}
obj.SetUnstructuredContent(map[string]interface{}{
"apiVersion": "appstudio.redhat.com/v1alpha1",
"kind": "EnterpriseContractPolicy",
"spec": map[string]interface{}{
"sources": []interface{}{
map[string]interface{}{
"volatileConfig": map[string]interface{}{
"exclude": []interface{}{
func() map[string]interface{} {
m := map[string]interface{}{
"value": "test-rule",
}
if !tt.omitField {
// Convert []string to []interface{}
componentNamesInterface := make([]interface{}, len(tt.componentNames))
for i, name := range tt.componentNames {
componentNamesInterface[i] = name
}
m["componentNames"] = componentNamesInterface
}
return m
}(),
},
},
},
},
},
})

// Validate the object
result := v.Validate(&obj)
isValid := result.IsValid()

if isValid != tt.wantValid {
t.Errorf("Validation for %v = %v, want %v. Errors: %v", tt.componentNames, isValid, tt.wantValid, result.Errors)
}

// Also validate the actual policy object
// Note: Empty arrays with omitempty are omitted during JSON marshaling,
// so they're treated as omitted fields and pass validation
if tt.wantValid && !tt.omitField {
policyObj := unstructured.Unstructured{}
policyBytes, err := json.Marshal(policy)
if err != nil {
t.Fatalf("unexpected error marshaling policy: %s", err)
}
if err := json.Unmarshal(policyBytes, &policyObj.Object); err != nil {
t.Fatalf("unexpected error unmarshaling policy: %s", err)
}

policyResult := v.Validate(&policyObj)
policyIsValid := policyResult.IsValid()

if policyIsValid != tt.wantValid {
t.Errorf("Policy validation for %v = %v, want %v. Errors: %v", tt.componentNames, policyIsValid, tt.wantValid, policyResult.Errors)
}
}
})
}
}
7 changes: 7 additions & 0 deletions api/v1alpha1/policy_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@
"type": "string",
"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,}$`"
},
"componentNames": {
"items": {
"type": "string"
},
"type": "array",
"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"
},
"reference": {
"type": "string",
"description": "Reference is used to include a link to related information such as a Jira issue URL.\n+optional"
Expand Down
13 changes: 11 additions & 2 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ spec:
items:
description: VolatileCriteria includes or excludes a policy rule with effective dates as an option.
properties:
componentNames:
description: |-
ComponentNames is used to specify component names from
ApplicationSnapshot. This allows filtering in scenarios where
multiple components share the same image repository.
items:
type: string
minLength: 1
minItems: 1
type: array
x-kubernetes-list-type: set
effectiveOn:
format: date-time
type: string
Expand Down Expand Up @@ -195,6 +206,17 @@ spec:
items:
description: VolatileCriteria includes or excludes a policy rule with effective dates as an option.
properties:
componentNames:
description: |-
ComponentNames is used to specify component names from
ApplicationSnapshot. This allows filtering in scenarios where
multiple components share the same image repository.
items:
type: string
minLength: 1
minItems: 1
type: array
x-kubernetes-list-type: set
effectiveOn:
format: date-time
type: string
Expand Down
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ Appears In: xref:{anchor_prefix}-github-com-enterprise-contract-enterprise-contr
ImageRef is used to specify an image by its digest. +
| *`imageDigest`* __string__ | ImageDigest is used to specify an image by its digest. +
| *`imageUrl`* __string__ | ImageUrl is used to specify an image by its URL without a tag. +
| *`componentNames`* __string array__ | ComponentNames is used to specify component names from +
ApplicationSnapshot. This allows filtering in scenarios where +
multiple components share the same image repository. +
| *`reference`* __string__ | Reference is used to include a link to related information such as a Jira issue URL. +
|===

Expand Down
Loading