Skip to content

Commit 1e60570

Browse files
authored
image-mapper: add matcher that identifies iamguarded images (#254)
Some iamguarded images have aliases for the upstream, non-bitnami variants. But not all. To provide a more consistent mapping behaviour, let's try and infer a match from upstream -> iamguarded. Not everyone is going to be interested in using our Helm charts, so add an option to ignore those matches. As part of this I've made the ignore functionality more modular so it's easier to add other ignore logic in future.
1 parent 41f843b commit 1e60570

File tree

9 files changed

+762
-37
lines changed

9 files changed

+762
-37
lines changed

image-mapper/README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ $ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/li
4949
```
5050

5151
```
52-
$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 bitnami/postgresql -o csv
52+
$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -o csv
5353
ghcr.io/stakater/reloader:v1.4.1,[stakater-reloader stakater-reloader-fips]
5454
registry.k8s.io/sig-storage/livenessprobe:v2.13.1,[kubernetes-csi-livenessprobe]
5555
```
@@ -59,11 +59,25 @@ the `--ignore-tiers` flag.
5959

6060
```
6161
$ ./image-mapper prom/prometheus
62-
prom/prometheus -> prometheus-fips
6362
prom/prometheus -> prometheus
63+
prom/prometheus -> prometheus-fips
64+
prom/prometheus -> prometheus-iamguarded
65+
prom/prometheus -> prometheus-iamguarded-fips
6466
6567
$ ./image-mapper prom/prometheus --ignore-tiers=FIPS
6668
prom/prometheus -> prometheus
69+
prom/prometheus -> prometheus-iamguarded
70+
```
71+
72+
The mapper will also return matches for our `-iamguarded` images. These images
73+
are designed specifically to work with Chainguard's Helm charts. If you aren't
74+
interested in using our charts, you can exclude those matches with
75+
`--ignore-iamguarded`.
76+
77+
```
78+
$ ./image-mapper prom/prometheus --ignore-iamguarded
79+
prom/prometheus -> prometheus
80+
prom/prometheus -> prometheus-fips
6781
```
6882

6983
## Docker

image-mapper/cmd/root.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import (
1010
)
1111

1212
var (
13-
outputFormat string
14-
ignoreTiers []string
13+
outputFormat string
14+
ignoreTiers []string
15+
ignoreIamguarded bool
1516
)
1617

1718
var rootCmd = &cobra.Command{
@@ -26,11 +27,14 @@ var rootCmd = &cobra.Command{
2627
return fmt.Errorf("constructing output: %w", err)
2728
}
2829

29-
var opts []mapper.Option
30+
var ignoreFns []mapper.IgnoreFn
3031
if len(ignoreTiers) > 0 {
31-
opts = append(opts, mapper.WithoutTiers(ignoreTiers))
32+
ignoreFns = append(ignoreFns, mapper.IgnoreTiers(ignoreTiers))
3233
}
33-
m, err := mapper.NewMapper(ctx, opts...)
34+
if ignoreIamguarded {
35+
ignoreFns = append(ignoreFns, mapper.IgnoreIamguarded())
36+
}
37+
m, err := mapper.NewMapper(ctx, mapper.WithIgnoreFns(ignoreFns...))
3438
if err != nil {
3539
return fmt.Errorf("creating mapper: %w", err)
3640
}
@@ -52,6 +56,7 @@ var rootCmd = &cobra.Command{
5256
func init() {
5357
rootCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format (csv, json, text, customer-yaml)")
5458
rootCmd.Flags().StringSliceVar(&ignoreTiers, "ignore-tiers", []string{}, "Ignore Chainguard repos of specific tiers (PREMIUM, APPLICATION, BASE, FIPS, AI)")
59+
rootCmd.Flags().BoolVar(&ignoreIamguarded, "ignore-iamguarded", false, "Ignore iamguarded images")
5560
}
5661

5762
func Execute() error {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package mapper
2+
3+
import (
4+
"slices"
5+
"strings"
6+
)
7+
8+
// IgnoreFn configures a mapper to ignore repositories
9+
type IgnoreFn func(Repo) bool
10+
11+
// IgnoreTiers ignores repos that are in the provided tiers
12+
func IgnoreTiers(tiers []string) IgnoreFn {
13+
var ignoreTiers []string
14+
for _, tier := range tiers {
15+
ignoreTiers = append(ignoreTiers, strings.ToLower(tier))
16+
}
17+
return func(repo Repo) bool {
18+
return slices.Contains(ignoreTiers, strings.ToLower(repo.CatalogTier))
19+
}
20+
}
21+
22+
// IgnoreIamguarded ignores iamguarded repos
23+
func IgnoreIamguarded() IgnoreFn {
24+
return func(repo Repo) bool {
25+
return strings.HasSuffix(repo.Name, "iamguarded") || strings.HasSuffix(repo.Name, "iamguarded-fips")
26+
}
27+
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package mapper
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestIgnoreTiers(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
tiers []string
11+
repo Repo
12+
wantIgnore bool
13+
}{
14+
{
15+
name: "exact match - FIPS",
16+
tiers: []string{"FIPS"},
17+
repo: Repo{
18+
Name: "test-repo",
19+
CatalogTier: "FIPS",
20+
},
21+
wantIgnore: true,
22+
},
23+
{
24+
name: "case insensitive match - lowercase tier input",
25+
tiers: []string{"fips"},
26+
repo: Repo{
27+
Name: "test-repo",
28+
CatalogTier: "FIPS",
29+
},
30+
wantIgnore: true,
31+
},
32+
{
33+
name: "case insensitive match - lowercase repo tier",
34+
tiers: []string{"FIPS"},
35+
repo: Repo{
36+
Name: "test-repo",
37+
CatalogTier: "fips",
38+
},
39+
wantIgnore: true,
40+
},
41+
{
42+
name: "case insensitive match - mixed case",
43+
tiers: []string{"FiPs"},
44+
repo: Repo{
45+
Name: "test-repo",
46+
CatalogTier: "fIpS",
47+
},
48+
wantIgnore: true,
49+
},
50+
{
51+
name: "multiple tiers - first matches",
52+
tiers: []string{"FIPS", "BASE", "APPLICATION"},
53+
repo: Repo{
54+
Name: "test-repo",
55+
CatalogTier: "FIPS",
56+
},
57+
wantIgnore: true,
58+
},
59+
{
60+
name: "multiple tiers - middle matches",
61+
tiers: []string{"FIPS", "BASE", "APPLICATION"},
62+
repo: Repo{
63+
Name: "test-repo",
64+
CatalogTier: "BASE",
65+
},
66+
wantIgnore: true,
67+
},
68+
{
69+
name: "multiple tiers - last matches",
70+
tiers: []string{"FIPS", "BASE", "APPLICATION"},
71+
repo: Repo{
72+
Name: "test-repo",
73+
CatalogTier: "APPLICATION",
74+
},
75+
wantIgnore: true,
76+
},
77+
{
78+
name: "no match",
79+
tiers: []string{"FIPS"},
80+
repo: Repo{
81+
Name: "test-repo",
82+
CatalogTier: "BASE",
83+
},
84+
wantIgnore: false,
85+
},
86+
{
87+
name: "multiple tiers - no match",
88+
tiers: []string{"FIPS", "BASE", "APPLICATION"},
89+
repo: Repo{
90+
Name: "test-repo",
91+
CatalogTier: "AI",
92+
},
93+
wantIgnore: false,
94+
},
95+
{
96+
name: "empty tier list",
97+
tiers: []string{},
98+
repo: Repo{
99+
Name: "test-repo",
100+
CatalogTier: "FIPS",
101+
},
102+
wantIgnore: false,
103+
},
104+
{
105+
name: "empty catalog tier",
106+
tiers: []string{"FIPS"},
107+
repo: Repo{
108+
Name: "test-repo",
109+
CatalogTier: "",
110+
},
111+
wantIgnore: false,
112+
},
113+
{
114+
name: "empty string in tiers list matches empty catalog tier",
115+
tiers: []string{""},
116+
repo: Repo{
117+
Name: "test-repo",
118+
CatalogTier: "",
119+
},
120+
wantIgnore: true,
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
ignoreFn := IgnoreTiers(tt.tiers)
127+
got := ignoreFn(tt.repo)
128+
if got != tt.wantIgnore {
129+
t.Errorf("IgnoreTiers() = %v, want %v", got, tt.wantIgnore)
130+
}
131+
})
132+
}
133+
}
134+
135+
func TestIgnoreIamguarded(t *testing.T) {
136+
tests := []struct {
137+
name string
138+
repo Repo
139+
wantIgnore bool
140+
}{
141+
{
142+
name: "repo ending with iamguarded",
143+
repo: Repo{
144+
Name: "test-repo-iamguarded",
145+
},
146+
wantIgnore: true,
147+
},
148+
{
149+
name: "repo ending with iamguarded-fips",
150+
repo: Repo{
151+
Name: "test-repo-iamguarded-fips",
152+
},
153+
wantIgnore: true,
154+
},
155+
{
156+
name: "repo with just iamguarded",
157+
repo: Repo{
158+
Name: "iamguarded",
159+
},
160+
wantIgnore: true,
161+
},
162+
{
163+
name: "repo with just iamguarded-fips",
164+
repo: Repo{
165+
Name: "iamguarded-fips",
166+
},
167+
wantIgnore: true,
168+
},
169+
{
170+
name: "repo not ending with iamguarded",
171+
repo: Repo{
172+
Name: "test-repo",
173+
},
174+
wantIgnore: false,
175+
},
176+
{
177+
name: "repo containing iamguarded but not at end",
178+
repo: Repo{
179+
Name: "iamguarded-test-repo",
180+
},
181+
wantIgnore: false,
182+
},
183+
{
184+
name: "repo containing iamguarded-fips but not at end",
185+
repo: Repo{
186+
Name: "iamguarded-fips-test",
187+
},
188+
wantIgnore: false,
189+
},
190+
{
191+
name: "empty repo name",
192+
repo: Repo{
193+
Name: "",
194+
},
195+
wantIgnore: false,
196+
},
197+
{
198+
name: "case sensitive - uppercase IAMGUARDED",
199+
repo: Repo{
200+
Name: "test-repo-IAMGUARDED",
201+
},
202+
wantIgnore: false,
203+
},
204+
{
205+
name: "case sensitive - uppercase IAMGUARDED-FIPS",
206+
repo: Repo{
207+
Name: "test-repo-IAMGUARDED-FIPS",
208+
},
209+
wantIgnore: false,
210+
},
211+
{
212+
name: "partial match - iamguarde (missing d)",
213+
repo: Repo{
214+
Name: "test-repo-iamguarde",
215+
},
216+
wantIgnore: false,
217+
},
218+
}
219+
220+
for _, tt := range tests {
221+
t.Run(tt.name, func(t *testing.T) {
222+
ignoreFn := IgnoreIamguarded()
223+
got := ignoreFn(tt.repo)
224+
if got != tt.wantIgnore {
225+
t.Errorf("IgnoreIamguarded() = %v, want %v", got, tt.wantIgnore)
226+
}
227+
})
228+
}
229+
}

image-mapper/internal/mapper/mapper.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"slices"
7-
"strings"
87

98
"github.com/google/go-containerregistry/pkg/name"
109
)
@@ -17,8 +16,8 @@ type Mapping struct {
1716

1817
// Mapper maps image references to images in our catalog
1918
type Mapper struct {
20-
repos []Repo
21-
ignoreTiers []string
19+
repos []Repo
20+
ignoreFns []IgnoreFn
2221
}
2322

2423
// NewMapper creates a new mapper
@@ -34,8 +33,8 @@ func NewMapper(ctx context.Context, opts ...Option) (*Mapper, error) {
3433
}
3534

3635
m := &Mapper{
37-
repos: repos,
38-
ignoreTiers: o.ignoreTiers,
36+
repos: repos,
37+
ignoreFns: o.ignoreFns,
3938
}
4039

4140
return m, nil
@@ -86,9 +85,7 @@ func (m *Mapper) Map(image string) (*Mapping, error) {
8685
continue
8786
}
8887

89-
// Exclude specific tiers. Useful for ignoring 'FIPS' tier
90-
// images when they aren't relevant.
91-
if slices.Contains(m.ignoreTiers, strings.ToLower(cgrrepo.CatalogTier)) {
88+
if m.ignoreRepo(cgrrepo) {
9289
continue
9390
}
9491

@@ -109,3 +106,14 @@ func (m *Mapper) Map(image string) (*Mapping, error) {
109106
Results: results,
110107
}, nil
111108
}
109+
110+
func (m *Mapper) ignoreRepo(repo Repo) bool {
111+
for _, ignore := range m.ignoreFns {
112+
if !ignore(repo) {
113+
continue
114+
}
115+
return true
116+
}
117+
118+
return false
119+
}

0 commit comments

Comments
 (0)