diff --git a/internal/evaluator/criteria.go b/internal/evaluator/criteria.go index c8f12f03b..2180965a0 100644 --- a/internal/evaluator/criteria.go +++ b/internal/evaluator/criteria.go @@ -18,6 +18,7 @@ package evaluator import ( "fmt" + "regexp" "time" ecc "github.com/conforma/crds/api/v1alpha1" @@ -106,6 +107,14 @@ func (c *Criteria) get(imageRef string, componentName string) []string { if componentItems, ok := c.componentItems[componentName]; ok { items = append(items, componentItems...) } + // For multi-arch image index components, also match by the original component + // name. During expansion, "foo" becomes "foo-sha256:-arm64" etc., but + // volatile config references the original name. + if origName := originalComponentName(componentName); origName != componentName { + if componentItems, ok := c.componentItems[origName]; ok { + items = append(items, componentItems...) + } + } } // Add any exceptions that pertain to all images. @@ -119,6 +128,19 @@ func (c *Criteria) getWithKey(key string) []string { return []string{} } +// multiArchSuffixRe matches the suffix appended to component names during multi-arch +// image index expansion (see applicationsnapshot/input.go imageIndexWorker). The format +// is "-sha256:-", e.g. "-sha256:abc123...-arm64". +var multiArchSuffixRe = regexp.MustCompile(`-sha256:[0-9a-f]{64}-\S+$`) + +// originalComponentName returns the component name before multi-arch image index +// expansion. For example, component "foo" is expanded into per-arch components like +// "foo-sha256:-arm64". This allows component-scoped volatile config +// (includes/excludes) to apply to per-arch components as well as the original. +func originalComponentName(componentName string) string { + return multiArchSuffixRe.ReplaceAllString(componentName, "") +} + func computeIncludeExclude(src ecc.Source, p ConfigProvider) (*Criteria, *Criteria) { include := &Criteria{} exclude := &Criteria{} diff --git a/internal/evaluator/criteria_test.go b/internal/evaluator/criteria_test.go index 446ea417f..a119e7b68 100644 --- a/internal/evaluator/criteria_test.go +++ b/internal/evaluator/criteria_test.go @@ -977,6 +977,45 @@ func TestCriteriaGetWithComponentName(t *testing.T) { componentName: "my-component", expected: []string{"test.image_check", "test.component_check", "*"}, }, + { + name: "Multi-arch expanded component matches original component name", + criteria: &Criteria{ + digestItems: map[string][]string{}, + componentItems: map[string][]string{ + "my-component": {"test.component_check"}, + }, + defaultItems: []string{"*"}, + }, + imageRef: "quay.io/repo/img@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + componentName: "my-component-sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890-arm64", + expected: []string{"test.component_check", "*"}, + }, + { + name: "Multi-arch expanded component with noarch suffix", + criteria: &Criteria{ + digestItems: map[string][]string{}, + componentItems: map[string][]string{ + "my-component": {"test.component_check"}, + }, + defaultItems: []string{"*"}, + }, + imageRef: "quay.io/repo/img@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + componentName: "my-component-sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890-noarch-2", + expected: []string{"test.component_check", "*"}, + }, + { + name: "Similar component name not incorrectly matched", + criteria: &Criteria{ + digestItems: map[string][]string{}, + componentItems: map[string][]string{ + "my-component": {"test.component_check"}, + }, + defaultItems: []string{"*"}, + }, + imageRef: "quay.io/repo/img@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + componentName: "my-component-libs", + expected: []string{"*"}, + }, } for _, tt := range tests { @@ -986,3 +1025,53 @@ func TestCriteriaGetWithComponentName(t *testing.T) { }) } } + +func TestOriginalComponentName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "plain component name unchanged", + input: "my-component", + expected: "my-component", + }, + { + name: "multi-arch arm64 suffix stripped", + input: "my-component-sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890-arm64", + expected: "my-component", + }, + { + name: "multi-arch amd64 suffix stripped", + input: "my-component-sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890-amd64", + expected: "my-component", + }, + { + name: "multi-arch noarch suffix stripped", + input: "my-component-sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890-noarch-2", + expected: "my-component", + }, + { + name: "similar name without digest not stripped", + input: "my-component-libs", + expected: "my-component-libs", + }, + { + name: "similar name with sha prefix but no digest not stripped", + input: "my-component-sha256", + expected: "my-component-sha256", + }, + { + name: "empty string unchanged", + input: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, originalComponentName(tt.input)) + }) + } +}