Skip to content

Commit 32f1819

Browse files
committed
Add image-mapper
This example matches non-Chainguard image references to images in the Chainguard catalog by leveraging the alias metadata in combination with a few simple heuristics.
1 parent 623a7e2 commit 32f1819

File tree

15 files changed

+1245
-0
lines changed

15 files changed

+1245
-0
lines changed

image-mapper/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
image-mapper
2+
images.txt

image-mapper/Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM cgr.dev/chainguard/go:latest AS builder
2+
3+
WORKDIR /app
4+
5+
COPY go.mod go.sum ./
6+
RUN go mod download
7+
8+
COPY main.go main.go
9+
COPY internal internal
10+
COPY cmd cmd
11+
12+
RUN CGO_ENABLED=0 go build -o image-mapper .
13+
14+
FROM cgr.dev/chainguard/static:latest
15+
16+
COPY --from=builder /app/image-mapper /usr/local/bin/image-mapper
17+
18+
ENTRYPOINT ["/usr/local/bin/image-mapper"]

image-mapper/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# image-mapper
2+
3+
An example of matching non-Chainguard images to their Chainguard equivalents.
4+
5+
## Usage
6+
7+
Build the tool.
8+
9+
```
10+
$ go build -o image-mapper .
11+
```
12+
13+
Then, provide the images to map on the command line.
14+
15+
```
16+
$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1
17+
ghcr.io/stakater/reloader:v1.4.1 -> stakater-reloader
18+
ghcr.io/stakater/reloader:v1.4.1 -> stakater-reloader-fips
19+
registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -> kubernetes-csi-livenessprobe
20+
```
21+
22+
You can provide a list of images (one image per line) via stdin when the first
23+
argument is `-`.
24+
25+
```
26+
$ cat ./images.txt | ./image-mapper -
27+
```
28+
29+
Configure the output format with the `-o` flag. Supported formats are: `csv`,
30+
`json` and `text`.
31+
32+
```
33+
$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -o json | jq -r .
34+
[
35+
{
36+
"image": "ghcr.io/stakater/reloader:v1.4.1",
37+
"results": [
38+
"stakater-reloader",
39+
"stakater-reloader-fips"
40+
]
41+
},
42+
{
43+
"image": "registry.k8s.io/sig-storage/livenessprobe:v2.13.1",
44+
"results": [
45+
"kubernetes-csi-livenessprobe"
46+
]
47+
}
48+
]
49+
```
50+
51+
```
52+
$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 bitnami/postgresql -o csv
53+
ghcr.io/stakater/reloader:v1.4.1,[stakater-reloader stakater-reloader-fips]
54+
registry.k8s.io/sig-storage/livenessprobe:v2.13.1,[kubernetes-csi-livenessprobe]
55+
```
56+
57+
The output will map both FIPS and non-FIPS variants. You can exclude FIPS with
58+
the `--ignore-tiers` flag.
59+
60+
```
61+
$ ./image-mapper prom/prometheus
62+
prom/prometheus -> prometheus-fips
63+
prom/prometheus -> prometheus
64+
65+
$ ./image-mapper prom/prometheus --ignore-tiers=FIPS
66+
prom/prometheus -> prometheus
67+
```
68+
69+
## Docker
70+
71+
```
72+
# Build the image
73+
docker build -t image-mapper .
74+
75+
# Run for an individual image
76+
docker run -it --rm image-mapper ghcr.io/stakater/reloader:v1.4.1
77+
78+
# Or, pass a list of images from a text file
79+
docker run -i --rm image-mapper -- - < images.txt
80+
```

image-mapper/cmd/root.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var (
13+
outputFormat string
14+
ignoreTiers []string
15+
)
16+
17+
var rootCmd = &cobra.Command{
18+
Use: "image-mapper",
19+
Short: "Map upstream image references to Chainguard images.",
20+
Args: cobra.MinimumNArgs(1),
21+
RunE: func(cmd *cobra.Command, args []string) error {
22+
ctx := context.Background()
23+
24+
output, err := mapper.NewOutput(outputFormat)
25+
if err != nil {
26+
return fmt.Errorf("constructing output: %w", err)
27+
}
28+
29+
var opts []mapper.Option
30+
if len(ignoreTiers) > 0 {
31+
opts = append(opts, mapper.WithoutTiers(ignoreTiers))
32+
}
33+
m, err := mapper.NewMapper(ctx, opts...)
34+
if err != nil {
35+
return fmt.Errorf("creating mapper: %w", err)
36+
}
37+
38+
it := mapper.NewArgsIterator(args)
39+
if args[0] == "-" {
40+
it = mapper.NewReaderIterator(os.Stdin)
41+
}
42+
43+
mappings, err := m.MapAll(it)
44+
if err != nil {
45+
return fmt.Errorf("mapping images: %w", err)
46+
}
47+
48+
return output(os.Stdout, mappings)
49+
},
50+
}
51+
52+
func init() {
53+
rootCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format (csv, json, text, customer-yaml)")
54+
rootCmd.Flags().StringSliceVar(&ignoreTiers, "ignore-tiers", []string{}, "Ignore Chainguard repos of specific tiers (PREMIUM, APPLICATION, BASE, FIPS, AI)")
55+
}
56+
57+
func Execute() error {
58+
return rootCmd.Execute()
59+
}

image-mapper/go.mod

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/chainguard-dev/customer-success/scripts/image-mapper
2+
3+
go 1.24.5
4+
5+
require (
6+
github.com/google/go-cmp v0.7.0
7+
github.com/google/go-containerregistry v0.20.6
8+
github.com/spf13/cobra v1.9.1
9+
)
10+
11+
require (
12+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
13+
github.com/opencontainers/go-digest v1.0.0 // indirect
14+
github.com/spf13/pflag v1.0.6 // indirect
15+
)

image-mapper/go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
3+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
4+
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
5+
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
6+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
7+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
8+
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
9+
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
10+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
11+
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
12+
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
13+
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
14+
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
15+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
16+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package mapper
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"io"
7+
)
8+
9+
// ErrIteratorDone indicates when an iterator is finished
10+
var ErrIteratorDone = errors.New("done")
11+
12+
// Iterator iterates over images
13+
type Iterator interface {
14+
Next() (string, error)
15+
}
16+
17+
type readerIterator struct {
18+
scanner *bufio.Scanner
19+
}
20+
21+
// NewReaderIterator iterates over images in the given reader. It expects an
22+
// image per line.
23+
func NewReaderIterator(r io.Reader) Iterator {
24+
return &readerIterator{
25+
scanner: bufio.NewScanner(r),
26+
}
27+
}
28+
29+
// Next returns the next line
30+
func (it *readerIterator) Next() (string, error) {
31+
if !it.scanner.Scan() {
32+
if it.scanner.Err() != nil {
33+
return "", it.scanner.Err()
34+
}
35+
36+
return "", ErrIteratorDone
37+
}
38+
39+
txt := it.scanner.Text()
40+
41+
// If the line is empty, skip to the next one
42+
if txt == "" {
43+
return it.Next()
44+
}
45+
46+
return txt, nil
47+
}
48+
49+
type argsIterator struct {
50+
args []string
51+
index int
52+
}
53+
54+
// NewArgsIterator iterates over a list of images
55+
func NewArgsIterator(args []string) Iterator {
56+
return &argsIterator{
57+
args: args,
58+
}
59+
}
60+
61+
// Next returns the next image
62+
func (it *argsIterator) Next() (string, error) {
63+
if it.index >= len(it.args) {
64+
return "", ErrIteratorDone
65+
}
66+
67+
arg := it.args[it.index]
68+
it.index++
69+
70+
return arg, nil
71+
}

0 commit comments

Comments
 (0)