diff --git a/image-mapper/README.md b/image-mapper/README.md index 4a11c05..0294acb 100644 --- a/image-mapper/README.md +++ b/image-mapper/README.md @@ -49,7 +49,7 @@ $ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/li ``` ``` -$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 bitnami/postgresql -o csv +$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -o csv ghcr.io/stakater/reloader:v1.4.1,[stakater-reloader stakater-reloader-fips] registry.k8s.io/sig-storage/livenessprobe:v2.13.1,[kubernetes-csi-livenessprobe] ``` @@ -59,11 +59,25 @@ the `--ignore-tiers` flag. ``` $ ./image-mapper prom/prometheus -prom/prometheus -> prometheus-fips prom/prometheus -> prometheus +prom/prometheus -> prometheus-fips +prom/prometheus -> prometheus-iamguarded +prom/prometheus -> prometheus-iamguarded-fips $ ./image-mapper prom/prometheus --ignore-tiers=FIPS prom/prometheus -> prometheus +prom/prometheus -> prometheus-iamguarded +``` + +The mapper will also return matches for our `-iamguarded` images. These images +are designed specifically to work with Chainguard's Helm charts. If you aren't +interested in using our charts, you can exclude those matches with +`--ignore-iamguarded`. + +``` +$ ./image-mapper prom/prometheus --ignore-iamguarded +prom/prometheus -> prometheus +prom/prometheus -> prometheus-fips ``` ## Docker diff --git a/image-mapper/cmd/root.go b/image-mapper/cmd/root.go index fcde657..a23efa2 100644 --- a/image-mapper/cmd/root.go +++ b/image-mapper/cmd/root.go @@ -10,8 +10,9 @@ import ( ) var ( - outputFormat string - ignoreTiers []string + outputFormat string + ignoreTiers []string + ignoreIamguarded bool ) var rootCmd = &cobra.Command{ @@ -26,11 +27,14 @@ var rootCmd = &cobra.Command{ return fmt.Errorf("constructing output: %w", err) } - var opts []mapper.Option + var ignoreFns []mapper.IgnoreFn if len(ignoreTiers) > 0 { - opts = append(opts, mapper.WithoutTiers(ignoreTiers)) + ignoreFns = append(ignoreFns, mapper.IgnoreTiers(ignoreTiers)) } - m, err := mapper.NewMapper(ctx, opts...) + if ignoreIamguarded { + ignoreFns = append(ignoreFns, mapper.IgnoreIamguarded()) + } + m, err := mapper.NewMapper(ctx, mapper.WithIgnoreFns(ignoreFns...)) if err != nil { return fmt.Errorf("creating mapper: %w", err) } @@ -52,6 +56,7 @@ var rootCmd = &cobra.Command{ func init() { rootCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format (csv, json, text, customer-yaml)") rootCmd.Flags().StringSliceVar(&ignoreTiers, "ignore-tiers", []string{}, "Ignore Chainguard repos of specific tiers (PREMIUM, APPLICATION, BASE, FIPS, AI)") + rootCmd.Flags().BoolVar(&ignoreIamguarded, "ignore-iamguarded", false, "Ignore iamguarded images") } func Execute() error { diff --git a/image-mapper/internal/mapper/ignore.go b/image-mapper/internal/mapper/ignore.go new file mode 100644 index 0000000..0b482d2 --- /dev/null +++ b/image-mapper/internal/mapper/ignore.go @@ -0,0 +1,27 @@ +package mapper + +import ( + "slices" + "strings" +) + +// IgnoreFn configures a mapper to ignore repositories +type IgnoreFn func(Repo) bool + +// IgnoreTiers ignores repos that are in the provided tiers +func IgnoreTiers(tiers []string) IgnoreFn { + var ignoreTiers []string + for _, tier := range tiers { + ignoreTiers = append(ignoreTiers, strings.ToLower(tier)) + } + return func(repo Repo) bool { + return slices.Contains(ignoreTiers, strings.ToLower(repo.CatalogTier)) + } +} + +// IgnoreIamguarded ignores iamguarded repos +func IgnoreIamguarded() IgnoreFn { + return func(repo Repo) bool { + return strings.HasSuffix(repo.Name, "iamguarded") || strings.HasSuffix(repo.Name, "iamguarded-fips") + } +} diff --git a/image-mapper/internal/mapper/ignore_test.go b/image-mapper/internal/mapper/ignore_test.go new file mode 100644 index 0000000..aa3de9b --- /dev/null +++ b/image-mapper/internal/mapper/ignore_test.go @@ -0,0 +1,229 @@ +package mapper + +import ( + "testing" +) + +func TestIgnoreTiers(t *testing.T) { + tests := []struct { + name string + tiers []string + repo Repo + wantIgnore bool + }{ + { + name: "exact match - FIPS", + tiers: []string{"FIPS"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "FIPS", + }, + wantIgnore: true, + }, + { + name: "case insensitive match - lowercase tier input", + tiers: []string{"fips"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "FIPS", + }, + wantIgnore: true, + }, + { + name: "case insensitive match - lowercase repo tier", + tiers: []string{"FIPS"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "fips", + }, + wantIgnore: true, + }, + { + name: "case insensitive match - mixed case", + tiers: []string{"FiPs"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "fIpS", + }, + wantIgnore: true, + }, + { + name: "multiple tiers - first matches", + tiers: []string{"FIPS", "BASE", "APPLICATION"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "FIPS", + }, + wantIgnore: true, + }, + { + name: "multiple tiers - middle matches", + tiers: []string{"FIPS", "BASE", "APPLICATION"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "BASE", + }, + wantIgnore: true, + }, + { + name: "multiple tiers - last matches", + tiers: []string{"FIPS", "BASE", "APPLICATION"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "APPLICATION", + }, + wantIgnore: true, + }, + { + name: "no match", + tiers: []string{"FIPS"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "BASE", + }, + wantIgnore: false, + }, + { + name: "multiple tiers - no match", + tiers: []string{"FIPS", "BASE", "APPLICATION"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "AI", + }, + wantIgnore: false, + }, + { + name: "empty tier list", + tiers: []string{}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "FIPS", + }, + wantIgnore: false, + }, + { + name: "empty catalog tier", + tiers: []string{"FIPS"}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "", + }, + wantIgnore: false, + }, + { + name: "empty string in tiers list matches empty catalog tier", + tiers: []string{""}, + repo: Repo{ + Name: "test-repo", + CatalogTier: "", + }, + wantIgnore: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ignoreFn := IgnoreTiers(tt.tiers) + got := ignoreFn(tt.repo) + if got != tt.wantIgnore { + t.Errorf("IgnoreTiers() = %v, want %v", got, tt.wantIgnore) + } + }) + } +} + +func TestIgnoreIamguarded(t *testing.T) { + tests := []struct { + name string + repo Repo + wantIgnore bool + }{ + { + name: "repo ending with iamguarded", + repo: Repo{ + Name: "test-repo-iamguarded", + }, + wantIgnore: true, + }, + { + name: "repo ending with iamguarded-fips", + repo: Repo{ + Name: "test-repo-iamguarded-fips", + }, + wantIgnore: true, + }, + { + name: "repo with just iamguarded", + repo: Repo{ + Name: "iamguarded", + }, + wantIgnore: true, + }, + { + name: "repo with just iamguarded-fips", + repo: Repo{ + Name: "iamguarded-fips", + }, + wantIgnore: true, + }, + { + name: "repo not ending with iamguarded", + repo: Repo{ + Name: "test-repo", + }, + wantIgnore: false, + }, + { + name: "repo containing iamguarded but not at end", + repo: Repo{ + Name: "iamguarded-test-repo", + }, + wantIgnore: false, + }, + { + name: "repo containing iamguarded-fips but not at end", + repo: Repo{ + Name: "iamguarded-fips-test", + }, + wantIgnore: false, + }, + { + name: "empty repo name", + repo: Repo{ + Name: "", + }, + wantIgnore: false, + }, + { + name: "case sensitive - uppercase IAMGUARDED", + repo: Repo{ + Name: "test-repo-IAMGUARDED", + }, + wantIgnore: false, + }, + { + name: "case sensitive - uppercase IAMGUARDED-FIPS", + repo: Repo{ + Name: "test-repo-IAMGUARDED-FIPS", + }, + wantIgnore: false, + }, + { + name: "partial match - iamguarde (missing d)", + repo: Repo{ + Name: "test-repo-iamguarde", + }, + wantIgnore: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ignoreFn := IgnoreIamguarded() + got := ignoreFn(tt.repo) + if got != tt.wantIgnore { + t.Errorf("IgnoreIamguarded() = %v, want %v", got, tt.wantIgnore) + } + }) + } +} diff --git a/image-mapper/internal/mapper/mapper.go b/image-mapper/internal/mapper/mapper.go index 67c497a..4457250 100644 --- a/image-mapper/internal/mapper/mapper.go +++ b/image-mapper/internal/mapper/mapper.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "slices" - "strings" "github.com/google/go-containerregistry/pkg/name" ) @@ -17,8 +16,8 @@ type Mapping struct { // Mapper maps image references to images in our catalog type Mapper struct { - repos []Repo - ignoreTiers []string + repos []Repo + ignoreFns []IgnoreFn } // NewMapper creates a new mapper @@ -34,8 +33,8 @@ func NewMapper(ctx context.Context, opts ...Option) (*Mapper, error) { } m := &Mapper{ - repos: repos, - ignoreTiers: o.ignoreTiers, + repos: repos, + ignoreFns: o.ignoreFns, } return m, nil @@ -86,9 +85,7 @@ func (m *Mapper) Map(image string) (*Mapping, error) { continue } - // Exclude specific tiers. Useful for ignoring 'FIPS' tier - // images when they aren't relevant. - if slices.Contains(m.ignoreTiers, strings.ToLower(cgrrepo.CatalogTier)) { + if m.ignoreRepo(cgrrepo) { continue } @@ -109,3 +106,14 @@ func (m *Mapper) Map(image string) (*Mapping, error) { Results: results, }, nil } + +func (m *Mapper) ignoreRepo(repo Repo) bool { + for _, ignore := range m.ignoreFns { + if !ignore(repo) { + continue + } + return true + } + + return false +} diff --git a/image-mapper/internal/mapper/mapper_test.go b/image-mapper/internal/mapper/mapper_test.go index 592bd1c..6055dc0 100644 --- a/image-mapper/internal/mapper/mapper_test.go +++ b/image-mapper/internal/mapper/mapper_test.go @@ -92,8 +92,8 @@ func TestMapperMap(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { m := &Mapper{ - repos: tc.repos, - ignoreTiers: []string{"fips"}, + repos: tc.repos, + ignoreFns: []IgnoreFn{IgnoreTiers([]string{"fips"})}, } result, err := m.Map(tc.image) @@ -139,8 +139,7 @@ func TestMapperMapAll(t *testing.T) { } m := &Mapper{ - repos: repos, - ignoreTiers: []string{}, + repos: repos, } images := []string{"nginx", "redis", "postgres"} @@ -186,8 +185,7 @@ func TestMapperMapAllDuplicates(t *testing.T) { } m := &Mapper{ - repos: repos, - ignoreTiers: []string{}, + repos: repos, } // Include duplicates in the input @@ -227,10 +225,8 @@ func TestMapperMapAllDuplicates(t *testing.T) { func TestMapperMapAllIteratorError(t *testing.T) { m := &Mapper{ - repos: []Repo{}, - ignoreTiers: []string{}, + repos: []Repo{}, } - expectedErr := errors.New("iterator error") iterator := &errorIterator{err: expectedErr} @@ -242,8 +238,7 @@ func TestMapperMapAllIteratorError(t *testing.T) { func TestMapperMapAllMapError(t *testing.T) { m := &Mapper{ - repos: []Repo{}, - ignoreTiers: []string{}, + repos: []Repo{}, } // Use an invalid image that will cause Map to fail @@ -265,6 +260,367 @@ func (it *errorIterator) Next() (string, error) { return "", it.err } +func TestMapperMapWithCustomIgnoreFn(t *testing.T) { + testCases := []struct { + name string + image string + repos []Repo + ignoreFns []IgnoreFn + expected *Mapping + }{ + { + name: "ignore repos by name prefix", + image: "nginx", + repos: []Repo{ + { + Name: "nginx", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + { + Name: "test-nginx", + CatalogTier: "APPLICATION", + Aliases: []string{"nginx"}, + }, + { + Name: "prod-nginx", + CatalogTier: "APPLICATION", + Aliases: []string{"nginx"}, + }, + }, + ignoreFns: []IgnoreFn{ + func(repo Repo) bool { + return strings.HasPrefix(repo.Name, "test-") + }, + }, + expected: &Mapping{ + Image: "nginx", + Results: []string{"nginx", "prod-nginx"}, + }, + }, + { + name: "ignore repos containing specific string", + image: "redis", + repos: []Repo{ + { + Name: "redis", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + { + Name: "redis-dev", + CatalogTier: "APPLICATION", + Aliases: []string{"redis"}, + }, + { + Name: "redis-prod", + CatalogTier: "APPLICATION", + Aliases: []string{"redis"}, + }, + }, + ignoreFns: []IgnoreFn{ + func(repo Repo) bool { + return strings.Contains(repo.Name, "-dev") + }, + }, + expected: &Mapping{ + Image: "redis", + Results: []string{"redis", "redis-prod"}, + }, + }, + { + name: "multiple custom ignore functions", + image: "postgres", + repos: []Repo{ + { + Name: "postgres", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + { + Name: "test-postgres", + CatalogTier: "APPLICATION", + Aliases: []string{"postgres"}, + }, + { + Name: "postgres-dev", + CatalogTier: "APPLICATION", + Aliases: []string{"postgres"}, + }, + { + Name: "postgres-prod", + CatalogTier: "APPLICATION", + Aliases: []string{"postgres"}, + }, + }, + ignoreFns: []IgnoreFn{ + func(repo Repo) bool { + return strings.HasPrefix(repo.Name, "test-") + }, + func(repo Repo) bool { + return strings.Contains(repo.Name, "-dev") + }, + }, + expected: &Mapping{ + Image: "postgres", + Results: []string{"postgres", "postgres-prod"}, + }, + }, + { + name: "ignore repos by exact name match", + image: "mysql", + repos: []Repo{ + { + Name: "mysql", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + { + Name: "mysql-legacy", + CatalogTier: "APPLICATION", + Aliases: []string{"mysql"}, + }, + { + Name: "mysql-new", + CatalogTier: "APPLICATION", + Aliases: []string{"mysql"}, + }, + }, + ignoreFns: []IgnoreFn{ + func(repo Repo) bool { + return repo.Name == "mysql-legacy" + }, + }, + expected: &Mapping{ + Image: "mysql", + Results: []string{"mysql", "mysql-new"}, + }, + }, + { + name: "ignore all matching repos with custom function", + image: "alpine", + repos: []Repo{ + { + Name: "alpine-dev", + CatalogTier: "APPLICATION", + Aliases: []string{"alpine"}, + }, + { + Name: "alpine-test", + CatalogTier: "APPLICATION", + Aliases: []string{"alpine"}, + }, + }, + ignoreFns: []IgnoreFn{ + func(repo Repo) bool { + return strings.HasPrefix(repo.Name, "alpine-") + }, + }, + expected: &Mapping{ + Image: "alpine", + Results: []string{}, + }, + }, + { + name: "combine built-in and custom ignore functions", + image: "node", + repos: []Repo{ + { + Name: "node", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + { + Name: "node-fips", + CatalogTier: "FIPS", + Aliases: []string{"node"}, + }, + { + Name: "experimental-node", + CatalogTier: "APPLICATION", + Aliases: []string{"node"}, + }, + { + Name: "node-staging", + CatalogTier: "APPLICATION", + Aliases: []string{"node"}, + }, + }, + ignoreFns: []IgnoreFn{ + IgnoreTiers([]string{"fips"}), + func(repo Repo) bool { + return strings.HasPrefix(repo.Name, "experimental-") + }, + }, + expected: &Mapping{ + Image: "node", + Results: []string{"node", "node-staging"}, + }, + }, + { + name: "ignore repos by suffix", + image: "python", + repos: []Repo{ + { + Name: "python", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + { + Name: "python-slim", + CatalogTier: "APPLICATION", + Aliases: []string{"python"}, + }, + { + Name: "python-alpine", + CatalogTier: "APPLICATION", + Aliases: []string{"python"}, + }, + }, + ignoreFns: []IgnoreFn{ + func(repo Repo) bool { + return strings.HasSuffix(repo.Name, "-alpine") + }, + }, + expected: &Mapping{ + Image: "python", + Results: []string{"python", "python-slim"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := &Mapper{ + repos: tc.repos, + ignoreFns: tc.ignoreFns, + } + + result, err := m.Map(tc.image) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + opts := cmpopts.SortSlices(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }) + + if diff := cmp.Diff(tc.expected, result, opts); diff != "" { + t.Errorf("mapping mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestMapperMapWithCustomIgnoreFnUsingAliases(t *testing.T) { + repos := []Repo{ + { + Name: "web-server", + CatalogTier: "APPLICATION", + Aliases: []string{"nginx", "httpd"}, + }, + { + Name: "cache-server", + CatalogTier: "APPLICATION", + Aliases: []string{"redis", "memcached"}, + }, + { + Name: "db-server", + CatalogTier: "APPLICATION", + Aliases: []string{"postgres", "mysql"}, + }, + } + + // Custom ignore function that checks aliases + ignoreFn := func(repo Repo) bool { + for _, alias := range repo.Aliases { + if alias == "redis" || alias == "memcached" { + return true + } + } + return false + } + + m := &Mapper{ + repos: repos, + ignoreFns: []IgnoreFn{ignoreFn}, + } + + // Should match cache-server but it should be ignored + result, err := m.Map("redis") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := &Mapping{ + Image: "redis", + Results: []string{}, + } + + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("mapping mismatch (-want +got):\n%s", diff) + } + + // Should match web-server and it should not be ignored + result, err = m.Map("nginx") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected = &Mapping{ + Image: "nginx", + Results: []string{"web-server"}, + } + + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("mapping mismatch (-want +got):\n%s", diff) + } +} + +func TestMapperMapWithNoIgnoreFns(t *testing.T) { + repos := []Repo{ + { + Name: "nginx", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + { + Name: "nginx-test", + CatalogTier: "APPLICATION", + Aliases: []string{"nginx"}, + }, + { + Name: "nginx-dev", + CatalogTier: "APPLICATION", + Aliases: []string{"nginx"}, + }, + } + + m := &Mapper{ + repos: repos, + ignoreFns: []IgnoreFn{}, // No ignore functions + } + + result, err := m.Map("nginx") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should get all matching repos when no ignore functions are set + expected := &Mapping{ + Image: "nginx", + Results: []string{"nginx", "nginx-dev", "nginx-test"}, + } + + opts := cmpopts.SortSlices(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }) + + if diff := cmp.Diff(expected, result, opts); diff != "" { + t.Errorf("mapping mismatch (-want +got):\n%s", diff) + } +} + func TestMapperIntegration(t *testing.T) { if v := os.Getenv("IMAGE_MAPPER_RUN_INTEGRATION_TESTS"); v == "" { t.Skip() @@ -294,6 +650,8 @@ func TestMapperIntegration(t *testing.T) { "ghcr.io/cloudnative-pg/pgbouncer:1.23.0": { "pgbouncer", "pgbouncer-fips", + "pgbouncer-iamguarded", + "pgbouncer-iamguarded-fips", }, "ghcr.io/crossplane-contrib/provider-aws-cloudformation:v1.20.1": { "crossplane-aws-cloudformation", @@ -401,6 +759,7 @@ func TestMapperIntegration(t *testing.T) { }, "influxdb:2.7.4-alpine": { "influxdb", + "influxdb-iamguarded", }, "oliver006/redis_exporter:v1.45.0-alpine": { "prometheus-redis-exporter", @@ -419,6 +778,8 @@ func TestMapperIntegration(t *testing.T) { "percona/haproxy:2.8.5": { "haproxy", "haproxy-fips", + "haproxy-iamguarded", + "haproxy-iamguarded-fips", }, "prom/mysqld-exporter:v0.16.0": { "prometheus-mysqld-exporter", @@ -430,6 +791,8 @@ func TestMapperIntegration(t *testing.T) { "quay.io/argoproj/argocd:v3.2.1": { "argocd", "argocd-fips", + "argocd-iamguarded", + "argocd-iamguarded-fips", "argocd-repo-server", "argocd-repo-server-fips", }, @@ -492,6 +855,8 @@ func TestMapperIntegration(t *testing.T) { "quay.io/minio/minio:RELEASE.2024-10-02T17-50-41Z": { "minio", "minio-fips", + "minio-iamguarded", + "minio-iamguarded-fips", }, "quay.io/minio/operator:v6.0.4": { "minio-operator", @@ -512,6 +877,8 @@ func TestMapperIntegration(t *testing.T) { "quay.io/prometheus/pushgateway:v1.9.0": { "prometheus-pushgateway", "prometheus-pushgateway-fips", + "prometheus-pushgateway-iamguarded", + "prometheus-pushgateway-iamguarded-fips", }, "registry.k8s.io/sig-storage/csi-attacher:v4.6.1": { "kubernetes-csi-external-attacher", @@ -544,6 +911,8 @@ func TestMapperIntegration(t *testing.T) { "valkey/valkey:7.2.5-alpine": { "valkey", "valkey-fips", + "valkey-iamguarded", + "valkey-iamguarded-fips", }, } diff --git a/image-mapper/internal/mapper/match.go b/image-mapper/internal/mapper/match.go index 4978f6c..222a2b0 100644 --- a/image-mapper/internal/mapper/match.go +++ b/image-mapper/internal/mapper/match.go @@ -28,6 +28,7 @@ type MatchFn func(ref name.Reference, repo Repo) bool var matchFns = []MatchFn{ matchBasename, matchDashname, + matchIamguarded, matchAliases, } @@ -60,6 +61,20 @@ func matchDashname(ref name.Reference, repo Repo) bool { return false } +// matchIamguarded matches Chainguard images that match the name of the upstream +// repository, with 'iamguarded' appended. This identifies iamguarded +// equivalents for upstream images. +func matchIamguarded(ref name.Reference, repo Repo) bool { + withIamguarded := fmt.Sprintf("%s-iamguarded", ref.Context().String()) + + iamguardedRef, err := name.ParseReference(withIamguarded) + if err != nil { + return false + } + + return matchBasename(iamguardedRef, repo) || matchDashname(iamguardedRef, repo) +} + // matchAliases uses the Chainguard repository's aliases to match against the // upstream reference func matchAliases(ref name.Reference, repo Repo) bool { diff --git a/image-mapper/internal/mapper/match_test.go b/image-mapper/internal/mapper/match_test.go index 3617c4a..69f53fb 100644 --- a/image-mapper/internal/mapper/match_test.go +++ b/image-mapper/internal/mapper/match_test.go @@ -53,6 +53,38 @@ func TestMatch(t *testing.T) { }, expected: false, }, + { + name: "basename match iamguarded", + refStr: "nginx", + repo: &Repo{ + Name: "nginx-iamguarded", + }, + expected: true, + }, + { + name: "basename match iamguarded fips", + refStr: "nginx", + repo: &Repo{ + Name: "nginx-iamguarded-fips", + }, + expected: true, + }, + { + name: "basename match iamguarded registry path", + refStr: "gcr.io/project/nginx", + repo: &Repo{ + Name: "nginx-iamguarded", + }, + expected: true, + }, + { + name: "basename match iamguarded fips registry path", + refStr: "gcr.io/project/nginx", + repo: &Repo{ + Name: "nginx-iamguarded-fips", + }, + expected: true, + }, { name: "basename match fips", refStr: "nginx", @@ -101,6 +133,38 @@ func TestMatch(t *testing.T) { }, expected: true, }, + { + name: "dashname match iamguarded", + refStr: "stakater/reloader", + repo: &Repo{ + Name: "stakater-reloader-iamguarded", + }, + expected: true, + }, + { + name: "dashname match iamguarded fips", + refStr: "stakater/reloader", + repo: &Repo{ + Name: "stakater-reloader-iamguarded-fips", + }, + expected: true, + }, + { + name: "dashname match iamguarded registry path", + refStr: "registry.example.com/stakater/reloader", + repo: &Repo{ + Name: "stakater-reloader-iamguarded", + }, + expected: true, + }, + { + name: "dashname match iamguarded fips registry path", + refStr: "registry.example.com/stakater/reloader", + repo: &Repo{ + Name: "stakater-reloader-iamguarded-fips", + }, + expected: true, + }, { name: "aliases exact repository match", refStr: "nginx", diff --git a/image-mapper/internal/mapper/options.go b/image-mapper/internal/mapper/options.go index 27345c7..cd82d54 100644 --- a/image-mapper/internal/mapper/options.go +++ b/image-mapper/internal/mapper/options.go @@ -1,22 +1,16 @@ package mapper -import "strings" - // Option configures a Mapper type Option func(*options) type options struct { - ignoreTiers []string + ignoreFns []IgnoreFn } -// WithoutTiers is a functional option that configures a Mapper to ignore -// Chainguard images of specific tiers -func WithoutTiers(tiers []string) Option { - var ignoreTiers []string - for _, tier := range tiers { - ignoreTiers = append(ignoreTiers, strings.ToLower(tier)) - } +// WithIgnoreFns is a functional option that configures the IgnoreFns used by +// the mapper +func WithIgnoreFns(ignoreFns ...IgnoreFn) Option { return func(o *options) { - o.ignoreTiers = ignoreTiers + o.ignoreFns = ignoreFns } }