diff --git a/image-mapper/README.md b/image-mapper/README.md index 3163e10..a9a7fd1 100644 --- a/image-mapper/README.md +++ b/image-mapper/README.md @@ -52,6 +52,32 @@ important, where possible, to use tags that are being actively maintained. Refer to [this page](./docs/map.md) for more details. +### Dockerfile + +The `dockerfile` subcommand maps image references in a Dockerfile to Chainguard. + +``` +$ cat Dockerfile +FROM python:3.13 + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] + +$ ./image-mapper map dockerfile Dockerfile +FROM cgr.dev/chainguard/python:3.13-dev + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] +``` + +Refer to [this page](./docs/map_dockerfile.md) for more details. + ### Helm The `helm-chart` and `helm-values` subcommands extract image related values and diff --git a/image-mapper/cmd/map.go b/image-mapper/cmd/map.go index c51bafd..bb851d9 100644 --- a/image-mapper/cmd/map.go +++ b/image-mapper/cmd/map.go @@ -63,6 +63,7 @@ func MapCommand() *cobra.Command { rootCmd.Flags().StringVar(&opts.Repo, "repository", "cgr.dev/chainguard", "Modifies the repository URI in the mappings. For instance, registry.internal.dev/chainguard would result in registry.internal.dev/chainguard/ in the output.") cmd.AddCommand( + MapDockerfileCommand(), MapHelmChartCommand(), MapHelmValuesCommand(), ) diff --git a/image-mapper/cmd/map_dockerfile.go b/image-mapper/cmd/map_dockerfile.go new file mode 100644 index 0000000..defdb33 --- /dev/null +++ b/image-mapper/cmd/map_dockerfile.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "fmt" + "io" + "os" + + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/dockerfile" + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" + "github.com/spf13/cobra" +) + +func MapDockerfileCommand() *cobra.Command { + opts := struct { + Repo string + }{} + cmd := &cobra.Command{ + Use: "dockerfile", + Short: "Map image references in a Dockerfile to their Chainguard equivalents.", + Example: ` +# Map a Dockerfile +image-mapper map dockerfile Dockerfile + +# Map a Dockerfile from stdin +cat Dockerfile | image-mapper map dockerfile - + +# Override the repository in the mappings with your own mirror or proxy. For instance, cgr.dev/chainguard/ would become registry.internal/cgr/ in the output. +image-mapper map dockerfile Dockerfile --repository=registry.internal/cgr +`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var ( + input []byte + err error + ) + switch args[0] { + case "-": + input, err = io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("reading stdin: %w", err) + } + default: + input, err = os.ReadFile(args[0]) + if err != nil { + return fmt.Errorf("reading file: %s: %w", args[0], err) + } + } + + output, err := dockerfile.Map(cmd.Context(), input, mapper.WithRepository(opts.Repo)) + if err != nil { + return fmt.Errorf("mapping dockerfile: %w", err) + } + + if _, err := os.Stdout.Write(output); err != nil { + return fmt.Errorf("writing output: %w", err) + } + + return nil + }, + } + + cmd.Flags().StringVar(&opts.Repo, "repository", "cgr.dev/chainguard", "Modifies the repository URI in the mappings. For instance, registry.internal.dev/chainguard would result in registry.internal.dev/chainguard/ in the output.") + + return cmd +} diff --git a/image-mapper/docs/map_dockerfile.md b/image-mapper/docs/map_dockerfile.md new file mode 100644 index 0000000..a42751d --- /dev/null +++ b/image-mapper/docs/map_dockerfile.md @@ -0,0 +1,107 @@ +# Map Dockerfile + +Map images references in a Dockerfile to Chainguard images. + +## How It Works + +The `dockerfile` subcommand maps any image references it finds in `FROM `, +`COPY --from=` or `RUN --mount-type=bind,from=` directives to +Chainguard. + +It will map images to `-dev` tags because they are more likely to work out of +the box as drop in replacements. + +## Basic Usage + +Given a `Dockerfile` like this: + +``` +FROM python:3.13 AS python + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] +``` + +Use the `dockerfile` subcommand to map it to Chainguard images. It returns the +result to stdout. + +``` +$ ./image-mapper map dockerfile Dockerfile +FROM cgr.dev/chainguard/python:3.13-dev + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] +``` + +You can also provide the Dockerfile via stdin: + +``` +$ cat Dockerfile | ./image-mapper map dockerfile - +``` + +## Repository Prefix + +Use the `--repository` flag to replace `cgr.dev/chainguard` with a custom +repository. + +``` +$ ./image-mapper map dockerfile Dockerfile --repository=registry.internal/cgr +FROM registry.internal/cgr/python:3.13-dev + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] +``` + +## Known Limitations + +There are a few rough edges that haven't been smoothed out yet. + +### Args + +The mapper supports resolving arguments to figure out which images they refer +to but it isn't clever enough to go back and update those arguments. + +For instance, a file like this: + +``` +ARG REGISTRY=docker.io +ARG IMAGE=library/python +ARG TAG=3.13 +FROM ${REGISTRY}/${IMAGE}:${TAG} +``` + +Would become: + +``` +ARG REGISTRY=docker.io +ARG IMAGE=library/python +ARG TAG=3.13 +FROM cgr.dev/chainguard/python:3.13-dev +``` + +### Multi Line Directives + +If it updates an image reference in a multi line directive then it will squash +the directive into one line. + +For instance, lines like this: + +``` +RUN --mount=type=bind,from=ubuntu,target=/bin/cat \ + cat run.py +``` + +Would become: + +``` +RUN --mount=type=bind,from=cgr.dev/chainguard/chainguard-base:latest,target=/bin/cat cat run.py +``` diff --git a/image-mapper/go.mod b/image-mapper/go.mod index 96331db..153ca15 100644 --- a/image-mapper/go.mod +++ b/image-mapper/go.mod @@ -5,6 +5,7 @@ go 1.24.5 require ( github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.6 + github.com/moby/buildkit v0.26.3 github.com/spf13/cobra v1.10.1 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.19.4 @@ -25,13 +26,14 @@ require ( github.com/containerd/containerd v1.7.29 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect + github.com/containerd/platforms v1.0.0-rc.2 // indirect + github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect @@ -54,14 +56,14 @@ require ( github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -77,6 +79,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -96,10 +99,10 @@ require ( golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.12.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/grpc v1.72.1 // indirect - google.golang.org/protobuf v1.36.5 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/api v0.34.2 // indirect diff --git a/image-mapper/go.sum b/image-mapper/go.sum index a48a687..66b5f54 100644 --- a/image-mapper/go.sum +++ b/image-mapper/go.sum @@ -32,6 +32,8 @@ github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuP github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= @@ -42,10 +44,12 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= +github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= +github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -76,8 +80,8 @@ github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -121,8 +125,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= @@ -135,8 +139,8 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -144,8 +148,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -158,8 +162,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -177,14 +181,10 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -197,6 +197,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/buildkit v0.26.3 h1:D+ruZVAk/3ipRq5XRxBH9/DIFpRjSlTtMbghT5gQP9g= +github.com/moby/buildkit v0.26.3/go.mod h1:4T4wJzQS4kYWIfFRjsbJry4QoxDBjK+UGOEOs1izL7w= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= @@ -227,19 +229,21 @@ github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1H github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= @@ -287,30 +291,30 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= @@ -321,18 +325,18 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsu go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= @@ -363,13 +367,10 @@ golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= @@ -378,8 +379,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -391,14 +392,14 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/image-mapper/internal/dockerfile/dockerfile.go b/image-mapper/internal/dockerfile/dockerfile.go new file mode 100644 index 0000000..5883f5c --- /dev/null +++ b/image-mapper/internal/dockerfile/dockerfile.go @@ -0,0 +1,238 @@ +package dockerfile + +import ( + "bytes" + "context" + "fmt" + "log" + "regexp" + "strings" + + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" + "github.com/moby/buildkit/frontend/dockerfile/parser" +) + +// Map images in a Dockerfile to their Chainguard equivalents +func Map(ctx context.Context, input []byte, opts ...mapper.Option) ([]byte, error) { + m, err := NewMapper(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("constructing mapper: %w", err) + } + + return mapDockerfile(m, input) +} + +func mapDockerfile(m mapper.Mapper, input []byte) ([]byte, error) { + res, err := parser.Parse(bytes.NewReader(input)) + if err != nil { + return nil, fmt.Errorf("parse dockerfile: %w", err) + } + + // Keep track of the name of the stages in the Dockerfile so we don't + // mistake a stage name for an image name in `COPY --from=` or + // RUN `--mount=type=bind,from=` style instructions. + stages := map[string]struct{}{} + + // Keep track of args so we can resolve them in `FROM` instructions. + args := map[string]string{} + + // Track when we hit the first `FROM` instruction, because any ARGs after that + // point aren't usable in `FROM` instructions. + beforeFrom := true + + // We'll compose the output by replacing lines in the input + output := string(input) + + // Track the number of lines we've removed so we can adjust the line + // numbers we modify accordingly + offset := 0 + + for _, child := range res.AST.Children { + var replacement string + + switch strings.ToLower(child.Value) { + + // ARG EXAMPLE= + case "arg": + if child.Next == nil { + continue + } + if !beforeFrom { + continue + } + + // Save the args, if there's a value + for n := child.Next; n != nil; n = n.Next { + parts := strings.Split(n.Value, "=") + if len(parts) == 2 { + args[parts[0]] = strings.Trim(parts[1], "\"") + } + } + + // FROM [AS ] + case "from": + beforeFrom = false + if child.Next == nil { + continue + } + + // Save the stage name, if there is one + for n := child.Next; n != nil; n = n.Next { + if strings.ToLower(n.Value) != "as" { + continue + } + if n.Next == nil { + continue + } + + stages[n.Next.Value] = struct{}{} + } + + // Resolve args in the FROM line + from := resolveArgs(args, child.Next.Value) + + // Map the image to Chainguard + img, err := mapper.MapImage(m, from) + if err != nil { + log.Printf("WARN: error mapping image: %s: %s", from, err) + continue + } + + replacement = strings.ReplaceAll(child.Original, child.Next.Value, img.String()) + + // COPY --from= + case "copy": + for _, flag := range child.Flags { + if !strings.HasPrefix(flag, "--from=") { + continue + } + + from := strings.TrimPrefix(flag, "--from=") + + // Skip if --from refers to a stage, rather than an image + if _, ok := stages[from]; ok { + continue + } + + img, err := mapper.MapImage(m, from) + if err != nil { + log.Printf("WARN: error mapping image: %s: %s", from, err) + continue + } + + replacement = strings.ReplaceAll(child.Original, flag, fmt.Sprintf("--from=%s", img)) + + break + } + + // RUN --mount=type=bind,target=/usr/bin,from=python + case "run": + original := child.Original + + for _, flag := range child.Flags { + if !strings.HasPrefix(flag, "--mount=") { + continue + } + + // Extract the image from a from= option + match := fromPattern.FindStringSubmatch(flag) + if len(match) < 2 { + continue + } + from := match[1] + + // Skip if from= refers to a stage, rather than + // an image + if _, ok := stages[from]; ok { + continue + } + + img, err := mapper.MapImage(m, from) + if err != nil { + log.Printf("WARN: error mapping image: %s: %s", from, err) + continue + } + // Replace the from= option in the flag + // with the mapped image + modifiedFlag := strings.ReplaceAll(flag, fmt.Sprintf("from=%s", from), fmt.Sprintf("from=%s", img)) + + // Replace the flag with the modified flag in + // the original line + original = strings.ReplaceAll(original, flag, modifiedFlag) + + } + + if original != child.Original { + replacement = original + } + } + + if replacement == "" { + continue + } + + // If the original instruction was spread over multiple lines + // then we will flatten it onto one line and remove the other + // lines to keep things tidy. + // + // One consequence of this is that we need to adjust the line + // numbers we write to after that point to account for the + // offset. + output = replaceLines(output, child.StartLine-offset, child.EndLine-offset, replacement) + offset = offset + (child.EndLine - child.StartLine) + } + + return []byte(output), nil +} + +// fromPattern extracts images in `from=` options in `RUN --mount` instructions +var fromPattern = regexp.MustCompile(`\bfrom=([^,]+)`) + +// argPattern identifies arguments like `${ARG_NAME}` +var argPattern = regexp.MustCompile(`\$\{([^}]+)\}`) + +// resolveArgs resolves args in a Dockerfile line +func resolveArgs(args map[string]string, line string) string { + return argPattern.ReplaceAllStringFunc(line, func(match string) string { + // Extract the inside of ${...} + content := match[2 : len(match)-1] + + // Check for default syntax: VAR:-default + argName := content + argDefault := "" + + parts := strings.Split(content, ":-") + if len(parts) > 1 { + argName = parts[0] + argDefault = parts[1] + } + + // If the variable exists in map, use it + if val, ok := args[argName]; ok { + return val + } + + // Otherwise, if a default is provided, use it + if argDefault != "" { + return argDefault + } + + // No variable and no default → return original pattern unchanged + return match + }) +} + +// replaceLines replaces the indicated lines in the output with the replacement +// value +func replaceLines(output string, start, end int, replacement string) string { + lines := strings.Split(output, "\n") + if start < 0 || end >= len(lines) || start > end { + return output + } + + lines[start-1] = replacement + + lines = append(lines[:start], lines[end:]...) + + return strings.Join(lines, "\n") +} diff --git a/image-mapper/internal/dockerfile/dockerfile_test.go b/image-mapper/internal/dockerfile/dockerfile_test.go new file mode 100644 index 0000000..9fa63ef --- /dev/null +++ b/image-mapper/internal/dockerfile/dockerfile_test.go @@ -0,0 +1,68 @@ +package dockerfile + +import ( + "fmt" + "os" + "testing" + + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" + "github.com/google/go-cmp/cmp" +) + +type mockMapper struct { + mappings map[string][]string +} + +func (m *mockMapper) Map(img string) (*mapper.Mapping, error) { + return &mapper.Mapping{ + Image: img, + Results: m.mappings[img], + }, nil +} + +func TestMapDockerfile(t *testing.T) { + m := &mockMapper{ + mappings: map[string][]string{ + "docker.io/python": { + "cgr.dev/chainguard/python:latest-dev", + }, + "python": { + "cgr.dev/chainguard/python:latest-dev", + }, + "python:3.13": { + "cgr.dev/chainguard/python:3.13-dev", + }, + }, + } + + testCases := map[string]struct{}{ + "singlestage": {}, + "multistage": {}, + "args": {}, + "copyfrom": {}, + "runmount": {}, + } + + for name := range testCases { + t.Run(name, func(t *testing.T) { + before, err := os.ReadFile(fmt.Sprintf("testdata/%s.before.Dockerfile", name)) + if err != nil { + t.Fatalf("unexpected error reading before file: %s", err) + } + + after, err := os.ReadFile(fmt.Sprintf("testdata/%s.after.Dockerfile", name)) + if err != nil { + t.Fatalf("unexpected error reading before file: %s", err) + } + + result, err := mapDockerfile(m, before) + if err != nil { + t.Fatalf("unexpected error mapping dockerfile: %s", err) + } + + if diff := cmp.Diff(after, result); diff != "" { + t.Errorf("unexpected result:\n%s", diff) + } + }) + } +} diff --git a/image-mapper/internal/dockerfile/mapper.go b/image-mapper/internal/dockerfile/mapper.go new file mode 100644 index 0000000..5c77088 --- /dev/null +++ b/image-mapper/internal/dockerfile/mapper.go @@ -0,0 +1,27 @@ +package dockerfile + +import ( + "context" + + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" +) + +// NewMapper returns a mapper.Mapper configured specifically for mapping images +// in Helm charts and values +func NewMapper(ctx context.Context, opts ...mapper.Option) (mapper.Mapper, error) { + defaultOpts := []mapper.Option{ + mapper.WithIgnoreFns( + // Iamguarded images are only designed to be + // used with our Helm charts. + mapper.IgnoreIamguarded(), + // TODO: make it possible select only + // FIPS images + mapper.IgnoreTiers([]string{"FIPS"}), + ), + // Use -dev tags because they're more likely to work out of the + // box + mapper.WithTagFilters(mapper.TagFilterPreferDev), + } + + return mapper.NewMapper(ctx, append(defaultOpts, opts...)...) +} diff --git a/image-mapper/internal/dockerfile/testdata/args.after.Dockerfile b/image-mapper/internal/dockerfile/testdata/args.after.Dockerfile new file mode 100644 index 0000000..98244e0 --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/args.after.Dockerfile @@ -0,0 +1,20 @@ +ARG IMAGE_REGISTRY +ARG IMAGE=python + +FROM cgr.dev/chainguard/python:latest-dev as latest +FROM ${IGNORED} as ignored +FROM cgr.dev/chainguard/python:3.13-dev AS python + +ARG IMAGE_REGISTRY=ignored + +COPY requirements.txt + +RUN pip install --no-cache-dir --target /app -r requirements.txt + +FROM cgr.dev/chainguard/python:latest-dev + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/args.before.Dockerfile b/image-mapper/internal/dockerfile/testdata/args.before.Dockerfile new file mode 100644 index 0000000..62eb295 --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/args.before.Dockerfile @@ -0,0 +1,20 @@ +ARG IMAGE_REGISTRY +ARG IMAGE=python + +FROM ${IMAGE} as latest +FROM ${IGNORED} as ignored +FROM ${IMAGE}:3.13 AS python + +ARG IMAGE_REGISTRY=ignored + +COPY requirements.txt + +RUN pip install --no-cache-dir --target /app -r requirements.txt + +FROM ${IMAGE_REGISTRY:-docker.io}/python + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/copyfrom.after.Dockerfile b/image-mapper/internal/dockerfile/testdata/copyfrom.after.Dockerfile new file mode 100644 index 0000000..861d1a8 --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/copyfrom.after.Dockerfile @@ -0,0 +1,18 @@ +FROM cgr.dev/chainguard/python:3.13-dev AS python + +WORKDIR /app + +COPY requirements.txt + +RUN pip install --no-cache-dir --target /app -r requirements.txt + +FROM cgr.dev/chainguard/python:3.13-dev + +COPY --from=python /app /app +COPY --from=cgr.dev/chainguard/python:3.13-dev /etc/example /example + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/copyfrom.before.Dockerfile b/image-mapper/internal/dockerfile/testdata/copyfrom.before.Dockerfile new file mode 100644 index 0000000..a9346cd --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/copyfrom.before.Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.13 AS python + +WORKDIR /app + +COPY requirements.txt + +RUN pip install --no-cache-dir --target /app -r requirements.txt + +FROM python:3.13 + +COPY --from=python /app /app +COPY --from=python:3.13 /etc/example /example + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/multistage.after.Dockerfile b/image-mapper/internal/dockerfile/testdata/multistage.after.Dockerfile new file mode 100644 index 0000000..4de684e --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/multistage.after.Dockerfile @@ -0,0 +1,17 @@ +FROM cgr.dev/chainguard/python:3.13-dev AS python + +WORKDIR /app + +COPY requirements.txt + +RUN pip install --no-cache-dir --target /app -r requirements.txt + +FROM cgr.dev/chainguard/python:3.13-dev + +COPY --from=python /app /app + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/multistage.before.Dockerfile b/image-mapper/internal/dockerfile/testdata/multistage.before.Dockerfile new file mode 100644 index 0000000..037ceed --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/multistage.before.Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.13 AS python + +WORKDIR /app + +COPY requirements.txt + +RUN pip install --no-cache-dir --target /app -r requirements.txt + +FROM python:3.13 + +COPY --from=python /app /app + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/runmount.after.Dockerfile b/image-mapper/internal/dockerfile/testdata/runmount.after.Dockerfile new file mode 100644 index 0000000..d971afc --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/runmount.after.Dockerfile @@ -0,0 +1,18 @@ +FROM cgr.dev/chainguard/python:3.13-dev AS python + +WORKDIR /app + +COPY requirements.txt + +RUN --mount=type=bind,from=python,target=/etc/example --mount=type=cache,target=/etc/pip,from=cgr.dev/chainguard/python:3.13-dev pip install --no-cache-dir --target /app -r requirements.txt && rm requirements.txt + +FROM cgr.dev/chainguard/python:latest-dev + +WORKDIR /app + +COPY run.py run.py + +RUN --mount=type=bind,from=cgr.dev/chainguard/python:latest-dev,target=/bin/cat cat run.py + + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/runmount.before.Dockerfile b/image-mapper/internal/dockerfile/testdata/runmount.before.Dockerfile new file mode 100644 index 0000000..837478a --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/runmount.before.Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.13 AS python + +WORKDIR /app + +COPY requirements.txt + +RUN --mount=type=bind,from=python,target=/etc/example \ + --mount=type=cache,target=/etc/pip,from=python:3.13 \ + pip install --no-cache-dir --target /app -r requirements.txt \ + && rm requirements.txt + +FROM python + +WORKDIR /app + +COPY run.py run.py + +RUN --mount=type=bind,from=docker.io/python,target=/bin/cat \ + cat run.py + + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/singlestage.after.Dockerfile b/image-mapper/internal/dockerfile/testdata/singlestage.after.Dockerfile new file mode 100644 index 0000000..a1b832b --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/singlestage.after.Dockerfile @@ -0,0 +1,11 @@ +FROM cgr.dev/chainguard/python:3.13-dev + +COPY requirements.txt + +RUN pip install --no-cache-dir -r requirements.txt + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/dockerfile/testdata/singlestage.before.Dockerfile b/image-mapper/internal/dockerfile/testdata/singlestage.before.Dockerfile new file mode 100644 index 0000000..14a0fbb --- /dev/null +++ b/image-mapper/internal/dockerfile/testdata/singlestage.before.Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.13 + +COPY requirements.txt + +RUN pip install --no-cache-dir -r requirements.txt + +WORKDIR /app + +COPY run.py run.py + +ENTRYPOINT ["python", "/app/run.py"] diff --git a/image-mapper/internal/helm/mapper.go b/image-mapper/internal/helm/mapper.go index 8fcf62d..332f7a6 100644 --- a/image-mapper/internal/helm/mapper.go +++ b/image-mapper/internal/helm/mapper.go @@ -2,7 +2,6 @@ package helm import ( "context" - "strings" "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" ) @@ -26,13 +25,8 @@ func NewMapper(ctx context.Context, opts ...mapper.Option) (mapper.Mapper, error ), // Our non-dev tags *should* be able to be // dropped into upstream helm - // charts, so let's prefer them by ensuring we - // don't match to -dev tags. - mapper.WithIncludeTags( - func(tag string) bool { - return !strings.HasSuffix(tag, "-dev") - }, - ), + // charts, so let's exclude them. + mapper.WithTagFilters(mapper.TagFilterExcludeDev), } return mapper.NewMapper(ctx, append(defaultOpts, opts...)...) diff --git a/image-mapper/internal/mapper/filter_tag.go b/image-mapper/internal/mapper/filter_tag.go index 6499adf..25b9dac 100644 --- a/image-mapper/internal/mapper/filter_tag.go +++ b/image-mapper/internal/mapper/filter_tag.go @@ -1,32 +1,66 @@ package mapper +import "strings" + // TagFilter is a function that filters tags -type TagFilter func(tag string) bool +type TagFilter func(tags []string) []string -func includeTags(tags []string, filters ...TagFilter) []string { - if len(filters) == 0 { - return tags - } - var output []string +// TagFilterExcludeDev excludes -dev tags from a list of tags +func TagFilterExcludeDev(tags []string) []string { + var out []string for _, tag := range tags { - if !includeTag(tag, filters...) { + if strings.HasSuffix(tag, "-dev") { continue } - output = append(output, tag) + out = append(out, tag) } - return output + return out } -func includeTag(tag string, filters ...TagFilter) bool { - for _, filter := range filters { - if !filter(tag) { +// TagFilterIncludeDev only includes -dev tags from a list of tags +func TagFilterIncludeDev(tags []string) []string { + var out []string + for _, tag := range tags { + if !strings.HasSuffix(tag, "-dev") { continue } - return true + out = append(out, tag) + } + + return out +} + +// TagFilterPreferDev returns only -dev tags if any exist, otherwise returns all tags +func TagFilterPreferDev(tags []string) []string { + var hasDev bool + for _, tag := range tags { + if strings.HasSuffix(tag, "-dev") { + hasDev = true + break + } + } + if !hasDev { + return tags + } + + return TagFilterIncludeDev(tags) +} + +func filterTags(repo Repo, filters ...TagFilter) []string { + tags := repo.ActiveTags + if len(repo.Tags) > 0 { + tags = flattenTags(repo.Tags) + } + if len(filters) == 0 { + return tags + } + + for _, filter := range filters { + tags = filter(tags) } - return false + return tags } diff --git a/image-mapper/internal/mapper/filter_tag_test.go b/image-mapper/internal/mapper/filter_tag_test.go index 46456cc..0d4a991 100644 --- a/image-mapper/internal/mapper/filter_tag_test.go +++ b/image-mapper/internal/mapper/filter_tag_test.go @@ -2,114 +2,168 @@ package mapper import ( "reflect" - "strings" "testing" ) -func TestIncludeTags(t *testing.T) { - tags := []string{"latest", "v1.0.0", "v2.0.0", "dev", "prod", "staging"} - +func TestTagFilterExcludeDev(t *testing.T) { tests := []struct { name string tags []string - filters []TagFilter expected []string }{ { - name: "no filters returns all tags", - tags: tags, - filters: nil, - expected: tags, + name: "empty tags", + tags: []string{}, + expected: nil, }, { - name: "empty filters returns all tags", - tags: tags, - filters: []TagFilter{}, - expected: tags, + name: "no dev tags", + tags: []string{"v1.0.0", "v2.0.0", "latest"}, + expected: []string{"v1.0.0", "v2.0.0", "latest"}, }, { - name: "single filter includes matching tags", - tags: tags, - filters: []TagFilter{ - func(tag string) bool { return strings.HasPrefix(tag, "v") }, - }, - expected: []string{"v1.0.0", "v2.0.0"}, + name: "all dev tags", + tags: []string{"v1.0.0-dev", "v2.0.0-dev", "latest-dev"}, + expected: nil, }, { - name: "multiple filters use OR logic", - tags: tags, - filters: []TagFilter{ - func(tag string) bool { return strings.HasPrefix(tag, "v") }, - func(tag string) bool { return tag == "dev" }, - }, - expected: []string{"v1.0.0", "v2.0.0", "dev"}, + name: "mixed dev and non-dev tags", + tags: []string{"v1.0.0", "v1.0.0-dev", "v2.0.0", "v2.0.0-dev", "latest"}, + expected: []string{"v1.0.0", "v2.0.0", "latest"}, }, { - name: "no tags match filters", - tags: tags, - filters: []TagFilter{ - func(tag string) bool { return strings.HasPrefix(tag, "nonexistent") }, - }, - expected: nil, + name: "tag containing dev but not ending with -dev", + tags: []string{"development", "v1.0.0-dev", "devops"}, + expected: []string{"development", "devops"}, }, { - name: "empty tags slice", - tags: []string{}, - filters: []TagFilter{ - func(tag string) bool { return true }, - }, + name: "single dev tag", + tags: []string{"v1.0.0-dev"}, expected: nil, }, { - name: "filter returns true for all", - tags: tags, - filters: []TagFilter{ - func(tag string) bool { return true }, - }, - expected: tags, + name: "single non-dev tag", + tags: []string{"v1.0.0"}, + expected: []string{"v1.0.0"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := TagFilterExcludeDev(tt.tags) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("TagFilterExcludeDev() = %v, expected %v", result, tt.expected) + } + }) + } +} + +func TestTagFilterIncludeDev(t *testing.T) { + tests := []struct { + name string + tags []string + expected []string + }{ + { + name: "empty tags", + tags: []string{}, + expected: nil, }, { - name: "filter returns false for all", - tags: tags, - filters: []TagFilter{ - func(tag string) bool { return false }, - }, + name: "no dev tags", + tags: []string{"v1.0.0", "v2.0.0", "latest"}, expected: nil, }, { - name: "three filters with different matches", - tags: []string{"alpha", "beta", "gamma", "delta"}, - filters: []TagFilter{ - func(tag string) bool { return tag == "alpha" }, - func(tag string) bool { return tag == "gamma" }, - func(tag string) bool { return strings.Contains(tag, "et") }, - }, - expected: []string{"alpha", "beta", "gamma"}, + name: "all dev tags", + tags: []string{"v1.0.0-dev", "v2.0.0-dev", "latest-dev"}, + expected: []string{"v1.0.0-dev", "v2.0.0-dev", "latest-dev"}, }, { - name: "filter by tag length", - tags: []string{"a", "ab", "abc", "abcd"}, - filters: []TagFilter{ - func(tag string) bool { return len(tag) > 2 }, - }, - expected: []string{"abc", "abcd"}, + name: "mixed dev and non-dev tags", + tags: []string{"v1.0.0", "v1.0.0-dev", "v2.0.0", "v2.0.0-dev", "latest"}, + expected: []string{"v1.0.0-dev", "v2.0.0-dev"}, }, { - name: "multiple filters where none match", - tags: tags, - filters: []TagFilter{ - func(tag string) bool { return strings.HasPrefix(tag, "x") }, - func(tag string) bool { return strings.HasPrefix(tag, "y") }, - }, + name: "tag containing dev but not ending with -dev", + tags: []string{"development", "v1.0.0-dev", "devops"}, + expected: []string{"v1.0.0-dev"}, + }, + { + name: "single dev tag", + tags: []string{"v1.0.0-dev"}, + expected: []string{"v1.0.0-dev"}, + }, + { + name: "single non-dev tag", + tags: []string{"v1.0.0"}, expected: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := includeTags(tt.tags, tt.filters...) + result := TagFilterIncludeDev(tt.tags) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("TagFilterIncludeDev() = %v, expected %v", result, tt.expected) + } + }) + } +} + +func TestTagFilterPreferDev(t *testing.T) { + tests := []struct { + name string + tags []string + expected []string + }{ + { + name: "empty tags", + tags: []string{}, + expected: []string{}, + }, + { + name: "no dev tags returns all tags", + tags: []string{"v1.0.0", "v2.0.0", "latest"}, + expected: []string{"v1.0.0", "v2.0.0", "latest"}, + }, + { + name: "all dev tags returns all tags", + tags: []string{"v1.0.0-dev", "v2.0.0-dev", "latest-dev"}, + expected: []string{"v1.0.0-dev", "v2.0.0-dev", "latest-dev"}, + }, + { + name: "mixed dev and non-dev tags returns only dev tags", + tags: []string{"v1.0.0", "v1.0.0-dev", "v2.0.0", "v2.0.0-dev", "latest"}, + expected: []string{"v1.0.0-dev", "v2.0.0-dev"}, + }, + { + name: "tag containing dev but not ending with -dev returns all", + tags: []string{"development", "devops", "production"}, + expected: []string{"development", "devops", "production"}, + }, + { + name: "single dev tag returns only dev tag", + tags: []string{"v1.0.0-dev"}, + expected: []string{"v1.0.0-dev"}, + }, + { + name: "single non-dev tag returns that tag", + tags: []string{"v1.0.0"}, + expected: []string{"v1.0.0"}, + }, + { + name: "one dev tag among many non-dev returns only dev", + tags: []string{"v1.0.0", "v2.0.0", "v3.0.0-dev", "v4.0.0", "latest"}, + expected: []string{"v3.0.0-dev"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := TagFilterPreferDev(tt.tags) if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("includeTags() = %v, expected %v", result, tt.expected) + t.Errorf("TagFilterPreferDev() = %v, expected %v", result, tt.expected) } }) } diff --git a/image-mapper/internal/mapper/mapper.go b/image-mapper/internal/mapper/mapper.go index 3a7ca38..4535fd4 100644 --- a/image-mapper/internal/mapper/mapper.go +++ b/image-mapper/internal/mapper/mapper.go @@ -21,10 +21,10 @@ type Mapper interface { } type mapper struct { - repos []Repo - ignoreFns []IgnoreFn - includeTags []TagFilter - repoName string + repos []Repo + ignoreFns []IgnoreFn + tagFilters []TagFilter + repoName string } // NewMapper creates a new mapper @@ -47,10 +47,10 @@ func NewMapper(ctx context.Context, opts ...Option) (*mapper, error) { } m := &mapper{ - repos: repos, - ignoreFns: o.ignoreFns, - includeTags: o.includeTags, - repoName: repoName, + repos: repos, + ignoreFns: o.ignoreFns, + tagFilters: o.tagFilters, + repoName: repoName, } return m, nil @@ -119,13 +119,8 @@ func (m *mapper) Map(image string) (*Mapping, error) { // Append the repository name to the rest of the reference result := fmt.Sprintf("%s/%s", m.repoName, cgrrepo.Name) - // Only match active tags unless we've fetched the full list of - // tags - tags := cgrrepo.ActiveTags - if len(cgrrepo.Tags) > 0 { - tags = flattenTags(cgrrepo.Tags) - } - tags = includeTags(tags, m.includeTags...) + // Filter the tags based on the configured filters + tags := filterTags(cgrrepo, m.tagFilters...) // Try and match the provided tag to one of the tags tag := MatchTag(tags, ref.TagStr()) diff --git a/image-mapper/internal/mapper/mapper_test.go b/image-mapper/internal/mapper/mapper_test.go index 41e5d1a..7222365 100644 --- a/image-mapper/internal/mapper/mapper_test.go +++ b/image-mapper/internal/mapper/mapper_test.go @@ -907,6 +907,7 @@ func TestMapperIntegration(t *testing.T) { }, "opensearchproject/opensearch-operator:2.7.0": { "cgr.dev/chainguard/opensearch-k8s-operator", + "cgr.dev/chainguard/opensearch-k8s-operator-fips", }, "opensearchproject/opensearch:2.19.1": { "cgr.dev/chainguard/opensearch", diff --git a/image-mapper/internal/mapper/options.go b/image-mapper/internal/mapper/options.go index 3a56797..1454088 100644 --- a/image-mapper/internal/mapper/options.go +++ b/image-mapper/internal/mapper/options.go @@ -7,7 +7,7 @@ type options struct { ignoreFns []IgnoreFn repo string inactiveTags bool - includeTags []TagFilter + tagFilters []TagFilter } // WithIgnoreFns is a functional option that configures the IgnoreFns used by @@ -26,11 +26,11 @@ func WithRepository(repo string) Option { } } -// WithIncludeTags is a functional option that configures filters that define -// which tags to include when matching tags -func WithIncludeTags(includeTags ...TagFilter) Option { +// WithTagFilters is a functional option that configures tag filters to apply to +// matches +func WithTagFilters(tagFilters ...TagFilter) Option { return func(o *options) { - o.includeTags = includeTags + o.tagFilters = tagFilters } }