From 771f9188bbe8bd9e6a4c32f6c46377830f9de5ab Mon Sep 17 00:00:00 2001 From: Rob Best Date: Tue, 23 Dec 2025 14:15:19 +0000 Subject: [PATCH 1/2] image-mapper: helm chart support When migrating a Helm chart to Chainguard it's a bit of a chore to figure out: 1. Which values you need to override 2. Which images you should replace the values with 3. Whether there are any tags you need to adjust. This change adds subcommands that streamline the process. This involves quite a lot of changes to the existing mapper: 1. Make it possible to match inactive tags by fetching the full list of tags from data.chaingurd.dev. Helm charts are tightly coupled to specific versions and therefore it isn't reasonable to expect users to bump a given version of a chart to active tags. Therefore, we need to aim to match the closest version possible. 2. Make it possible to map to a custom repository prefix. Chances are, users are using an internal mirror or proxy when they deploy images via Helm. 3. Make it possible for the mapper to ignore certain tags. We typically don't want/need to use -dev tags in Helm charts as our non-dev images *should* drop in nicely. 4. Nest all the commands under a `map` command. This has nicer semantics. 5. Overhaul the docs to account for multiple commands. I've also had to tweak and fix some tests and other bits that I found while I was testing the Helm feature. --- image-mapper/README.md | 128 +++-- image-mapper/cmd/map.go | 74 +++ image-mapper/cmd/map_helm.go | 123 +++++ image-mapper/cmd/root.go | 54 +-- image-mapper/docs/map.md | 83 ++++ image-mapper/docs/map_helm.md | 134 ++++++ image-mapper/go.mod | 112 ++++- image-mapper/go.sum | 443 +++++++++++++++++- image-mapper/internal/helm/chart.go | 177 +++++++ image-mapper/internal/helm/chart_test.go | 148 ++++++ image-mapper/internal/helm/mapper.go | 39 ++ .../helm/testdata/test-chart/Chart.yaml | 21 + .../charts/test-subchart/Chart.yaml | 16 + .../charts/test-subchart/values.yaml | 42 ++ .../helm/testdata/test-chart/values.yaml | 61 +++ image-mapper/internal/helm/values.go | 268 +++++++++++ image-mapper/internal/helm/values_test.go | 139 ++++++ image-mapper/internal/mapper/filter_tag.go | 32 ++ .../internal/mapper/filter_tag_test.go | 116 +++++ image-mapper/internal/mapper/mapper.go | 74 ++- image-mapper/internal/mapper/mapper_test.go | 150 +++++- image-mapper/internal/mapper/match_tag.go | 69 +-- .../internal/mapper/match_tag_test.go | 27 +- image-mapper/internal/mapper/options.go | 29 +- image-mapper/internal/mapper/repos.go | 127 ++++- image-mapper/internal/yamlhelpers/add.go | 56 +++ image-mapper/internal/yamlhelpers/add_test.go | 378 +++++++++++++++ image-mapper/internal/yamlhelpers/walk.go | 37 ++ .../internal/yamlhelpers/walk_test.go | 297 ++++++++++++ 29 files changed, 3258 insertions(+), 196 deletions(-) create mode 100644 image-mapper/cmd/map.go create mode 100644 image-mapper/cmd/map_helm.go create mode 100644 image-mapper/docs/map.md create mode 100644 image-mapper/docs/map_helm.md create mode 100644 image-mapper/internal/helm/chart.go create mode 100644 image-mapper/internal/helm/chart_test.go create mode 100644 image-mapper/internal/helm/mapper.go create mode 100644 image-mapper/internal/helm/testdata/test-chart/Chart.yaml create mode 100644 image-mapper/internal/helm/testdata/test-chart/charts/test-subchart/Chart.yaml create mode 100644 image-mapper/internal/helm/testdata/test-chart/charts/test-subchart/values.yaml create mode 100644 image-mapper/internal/helm/testdata/test-chart/values.yaml create mode 100644 image-mapper/internal/helm/values.go create mode 100644 image-mapper/internal/helm/values_test.go create mode 100644 image-mapper/internal/mapper/filter_tag.go create mode 100644 image-mapper/internal/mapper/filter_tag_test.go create mode 100644 image-mapper/internal/yamlhelpers/add.go create mode 100644 image-mapper/internal/yamlhelpers/add_test.go create mode 100644 image-mapper/internal/yamlhelpers/walk.go create mode 100644 image-mapper/internal/yamlhelpers/walk_test.go diff --git a/image-mapper/README.md b/image-mapper/README.md index eea1aa3..3163e10 100644 --- a/image-mapper/README.md +++ b/image-mapper/README.md @@ -2,7 +2,7 @@ A tool for matching non-Chainguard images to their Chainguard equivalents. -## Usage +## Build Build the tool. @@ -10,100 +10,88 @@ Build the tool. $ go build -o image-mapper . ``` -Then, provide the images to map on the command line. +You can also build and run the tool with Docker. ``` -$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -ghcr.io/stakater/reloader:v1.4.1 -> cgr.dev/chainguard/stakater-reloader-fips:v1.4.12 -ghcr.io/stakater/reloader:v1.4.1 -> cgr.dev/chainguard/stakater-reloader:v1.4.12 -registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -> cgr.dev/chainguard/kubernetes-csi-livenessprobe:v2.17.0 -``` - -You'll notice that the mapper increments the tag to the closest version -supported by Chainguard. To benefit from continued CVE remediation, it's -important, where possible, to use tags that are being actively maintained. +# Build the image +docker build -t image-mapper . -You can also provide a list of images (one image per line) via stdin when the first -argument is `-`. +# Run for an individual image +docker run -it --rm image-mapper map ghcr.io/stakater/reloader:v1.4.1 -``` -$ cat ./images.txt | ./image-mapper - +# Or, pass a list of images from a text file +docker run -i --rm image-mapper -- map - < images.txt ``` -## Options +## Basic Usage -### Output +### Map -Configure the output format with the `-o` flag. Supported formats are: `csv`, -`json` and `text`. +The `map` command maps images provided on the command line. ``` -$ ./image-mapper ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -o json | jq -r . -[ - { - "image": "ghcr.io/stakater/reloader:v1.4.1", - "results": [ - "cgr.dev/chainguard/stakater-reloader-fips:v1.4.12", - "cgr.dev/chainguard/stakater-reloader:v1.4.12" - ] - }, - { - "image": "registry.k8s.io/sig-storage/livenessprobe:v2.13.1", - "results": [ - "cgr.dev/chainguard/kubernetes-csi-livenessprobe:v2.17.0" - ] - } -] +$ ./image-mapper map ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1 +ghcr.io/stakater/reloader:v1.4.1 -> cgr.dev/chainguard/stakater-reloader-fips:v1.4.12 +ghcr.io/stakater/reloader:v1.4.1 -> cgr.dev/chainguard/stakater-reloader:v1.4.12 +registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -> cgr.dev/chainguard/kubernetes-csi-livenessprobe:v2.17.0 ``` +You can also provide a list of images (one image per line) via stdin when the first +argument is `-`. + ``` -$ ./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,[cgr.dev/chainguard/stakater-reloader-fips:v1.4.12 cgr.dev/chainguard/stakater-reloader:v1.4.12] -registry.k8s.io/sig-storage/livenessprobe:v2.13.1,[cgr.dev/chainguard/kubernetes-csi-livenessprobe:v2.17.0] +$ cat ./images.txt | ./image-mapper map - +ghcr.io/stakater/reloader:v1.4.1 -> cgr.dev/chainguard/stakater-reloader-fips:v1.4.12 +ghcr.io/stakater/reloader:v1.4.1 -> cgr.dev/chainguard/stakater-reloader:v1.4.12 +registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -> cgr.dev/chainguard/kubernetes-csi-livenessprobe:v2.17.0 ``` -### Ignore Tiers (i.e FIPS) - -The output will map both FIPS and non-FIPS variants. You can exclude FIPS with -the `--ignore-tiers` flag. +You'll notice that the mapper increments the tag to the closest version +supported by Chainguard. To benefit from continued CVE remediation, it's +important, where possible, to use tags that are being actively maintained. -``` -$ ./image-mapper prom/prometheus -prom/prometheus -> cgr.dev/chainguard/prometheus-fips:latest -prom/prometheus -> cgr.dev/chainguard/prometheus-iamguarded-fips:latest -prom/prometheus -> cgr.dev/chainguard/prometheus-iamguarded:latest -prom/prometheus -> cgr.dev/chainguard/prometheus:latest - -$ ./image-mapper prom/prometheus --ignore-tiers=FIPS -prom/prometheus -> cgr.dev/chainguard/prometheus-iamguarded:latest -prom/prometheus -> cgr.dev/chainguard/prometheus:latest -``` +Refer to [this page](./docs/map.md) for more details. -### Ignore Iamguarded +### Helm -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`. +The `helm-chart` and `helm-values` subcommands extract image related values and +map them to Chainguard. ``` -$ ./image-mapper prom/prometheus --ignore-iamguarded -prom/prometheus -> cgr.dev/chainguard/prometheus-fips:latest -prom/prometheus -> cgr.dev/chainguard/prometheus:latest +$ ./image-mapper map helm-chart argocd/argo-cd +redis-ha: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.2 # Original: 8.2.2-alpine + configmapTest: + image: + repository: cgr.dev/chainguard/shellcheck # Original: koalaman/shellcheck + tag: v0.11.0-dev # Original: v0.10.0 + haproxy: + image: + repository: cgr.dev/chainguard/haproxy # Original: ecr-public.aws.com/docker/library/haproxy + exporter: + image: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter +global: + image: + repository: cgr.dev/chainguard/argocd # Original: quay.io/argoproj/argocd +... ``` -## Docker - ``` -# Build the image -docker build -t image-mapper . +$ helm show values argocd/argo-cd | ./image-mapper map helm-values - +global: + image: + repository: cgr.dev/chainguard/argocd # Original: quay.io/argoproj/argocd +dex: + image: + repository: cgr.dev/chainguard/dex # Original: ghcr.io/dexidp/dex +... +``` -# Run for an individual image -docker run -it --rm image-mapper ghcr.io/stakater/reloader:v1.4.1 +These commands provide values overrides that you can pass to `helm install`. -# Or, pass a list of images from a text file -docker run -i --rm image-mapper -- - < images.txt -``` +Refer to [this page](./docs/map_helm.md) for more details. ## Development diff --git a/image-mapper/cmd/map.go b/image-mapper/cmd/map.go new file mode 100644 index 0000000..51851b7 --- /dev/null +++ b/image-mapper/cmd/map.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand( + MapCommand(), + ) +} + +func MapCommand() *cobra.Command { + opts := struct { + OutputFormat string + IgnoreTiers []string + IgnoreIamguarded bool + Repo string + }{} + cmd := &cobra.Command{ + Use: "map", + Short: "Map upstream image references to Chainguard images.", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + output, err := mapper.NewOutput(opts.OutputFormat) + if err != nil { + return fmt.Errorf("constructing output: %w", err) + } + + var ignoreFns []mapper.IgnoreFn + if len(opts.IgnoreTiers) > 0 { + ignoreFns = append(ignoreFns, mapper.IgnoreTiers(opts.IgnoreTiers)) + } + if opts.IgnoreIamguarded { + ignoreFns = append(ignoreFns, mapper.IgnoreIamguarded()) + } + m, err := mapper.NewMapper(ctx, mapper.WithRepository(opts.Repo), mapper.WithIgnoreFns(ignoreFns...)) + if err != nil { + return fmt.Errorf("creating mapper: %w", err) + } + + it := mapper.NewArgsIterator(args) + if args[0] == "-" { + it = mapper.NewReaderIterator(os.Stdin) + } + + mappings, err := m.MapAll(it) + if err != nil { + return fmt.Errorf("mapping images: %w", err) + } + + return output(os.Stdout, mappings) + }, + } + + rootCmd.Flags().StringVarP(&opts.OutputFormat, "output", "o", "text", "Output format (csv, json, text, customer-yaml)") + rootCmd.Flags().StringSliceVar(&opts.IgnoreTiers, "ignore-tiers", []string{}, "Ignore Chainguard repos of specific tiers (PREMIUM, APPLICATION, BASE, FIPS, AI)") + rootCmd.Flags().BoolVar(&opts.IgnoreIamguarded, "ignore-iamguarded", false, "Ignore iamguarded images") + 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( + MapHelmChartCommand(), + MapHelmValuesCommand(), + ) + + return cmd +} diff --git a/image-mapper/cmd/map_helm.go b/image-mapper/cmd/map_helm.go new file mode 100644 index 0000000..b8caaa9 --- /dev/null +++ b/image-mapper/cmd/map_helm.go @@ -0,0 +1,123 @@ +package cmd + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/helm" + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" + "github.com/spf13/cobra" +) + +func MapHelmChartCommand() *cobra.Command { + opts := struct { + Repo string + ChartRepo string + ChartVersion string + }{} + cmd := &cobra.Command{ + Use: "helm-chart", + Short: "Extract image related values from a Helm chart and map them to Chainguard.", + Example: ` + # Map a Helm chart. This requires that the Chart repo has been added with 'helm repo add' beforehand. + image-mapper map helm-chart argocd/argo-cd + + # 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 helm-chart argocd/argo-cd --repository=registry.internal/cgr + + # Map a specific version of a Helm chart. + image-mapper map helm-chart argocd/argo-cd --chart-version=9.0.0 + + # Specify a remote Chart repostory. This means the repository doesn't need to be added with 'helm repo add'. + image-mapper map helm-chart argo-cd --chart-repo=https://argoproj.github.io/argo-helm + + # Specify a specific version of a remote Chart. + image-mapper map helm-chart argo-cd --chart-repo=https://argoproj.github.io/argo-helm --chart-version=9.0.0 +`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + chart := helm.ChartDescriptor{ + Name: args[0], + Repository: opts.ChartRepo, + Version: opts.ChartVersion, + } + output, err := helm.MapChart(ctx, chart, mapper.WithRepository(opts.Repo)) + if err != nil { + return fmt.Errorf("mapping values: %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.") + cmd.Flags().StringVar(&opts.ChartRepo, "chart-repo", "", "The chart repository url to locate the requested chart.") + cmd.Flags().StringVar(&opts.ChartVersion, "chart-version", "", "A version constraint for the chart version.") + + return cmd +} + +func MapHelmValuesCommand() *cobra.Command { + opts := struct { + Repo string + }{} + cmd := &cobra.Command{ + Use: "helm-values", + Short: "Extract image related values from a Helm values file and map them to Chainguard.", + Example: ` + # Map images in the values returned by 'helm show values' + helm show values argocd/argo-cd | image-mapper map helm-values - + + # Map images in a values file on disk. + helm show values argocd/argo-cd > values.yaml + image-mapper map helm-values values.yaml + + # 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 helm-values values.yaml --repository=registry.internal/cgr +`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + 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 := helm.MapValues(ctx, input, mapper.WithRepository(opts.Repo)) + if err != nil { + return fmt.Errorf("mapping values: %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/cmd/root.go b/image-mapper/cmd/root.go index a23efa2..e33c3d5 100644 --- a/image-mapper/cmd/root.go +++ b/image-mapper/cmd/root.go @@ -1,62 +1,10 @@ package cmd -import ( - "context" - "fmt" - "os" - - "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" - "github.com/spf13/cobra" -) - -var ( - outputFormat string - ignoreTiers []string - ignoreIamguarded bool -) +import "github.com/spf13/cobra" var rootCmd = &cobra.Command{ Use: "image-mapper", Short: "Map upstream image references to Chainguard images.", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.Background() - - output, err := mapper.NewOutput(outputFormat) - if err != nil { - return fmt.Errorf("constructing output: %w", err) - } - - var ignoreFns []mapper.IgnoreFn - if len(ignoreTiers) > 0 { - ignoreFns = append(ignoreFns, mapper.IgnoreTiers(ignoreTiers)) - } - 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) - } - - it := mapper.NewArgsIterator(args) - if args[0] == "-" { - it = mapper.NewReaderIterator(os.Stdin) - } - - mappings, err := m.MapAll(it) - if err != nil { - return fmt.Errorf("mapping images: %w", err) - } - - return output(os.Stdout, mappings) - }, -} - -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/docs/map.md b/image-mapper/docs/map.md new file mode 100644 index 0000000..da9f74c --- /dev/null +++ b/image-mapper/docs/map.md @@ -0,0 +1,83 @@ +# Map + +The `map` command maps image references to their Chainguard equivalents. + +## Usage + +You can pass images on the command line. + +``` +$ ./image-mapper map ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1 +ghcr.io/stakater/reloader:v1.4.1 -> cgr.dev/chainguard/stakater-reloader-fips:v1.4.12 +ghcr.io/stakater/reloader:v1.4.1 -> cgr.dev/chainguard/stakater-reloader:v1.4.12 +registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -> cgr.dev/chainguard/kubernetes-csi-livenessprobe:v2.17.0 +``` + +You can also provide a list of images (one image per line) via stdin when the first +argument is `-`. + +``` +$ cat ./images.txt | ./image-mapper map - +``` + +## Options + +### Output + +Configure the output format with the `-o` flag. Supported formats are: `csv`, +`json` and `text`. + +``` +$ ./image-mapper map ghcr.io/stakater/reloader:v1.4.1 registry.k8s.io/sig-storage/livenessprobe:v2.13.1 -o json | jq -r . +[ + { + "image": "ghcr.io/stakater/reloader:v1.4.1", + "results": [ + "cgr.dev/chainguard/stakater-reloader-fips:v1.4.12", + "cgr.dev/chainguard/stakater-reloader:v1.4.12" + ] + }, + { + "image": "registry.k8s.io/sig-storage/livenessprobe:v2.13.1", + "results": [ + "cgr.dev/chainguard/kubernetes-csi-livenessprobe:v2.17.0" + ] + } +] +``` + +``` +$ ./image-mapper map 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,[cgr.dev/chainguard/stakater-reloader-fips:v1.4.12 cgr.dev/chainguard/stakater-reloader:v1.4.12] +registry.k8s.io/sig-storage/livenessprobe:v2.13.1,[cgr.dev/chainguard/kubernetes-csi-livenessprobe:v2.17.0] +``` + +### Ignore Tiers (i.e FIPS) + +The output will map both FIPS and non-FIPS variants. You can exclude FIPS with +the `--ignore-tiers` flag. + +``` +$ ./image-mapper map prom/prometheus +prom/prometheus -> cgr.dev/chainguard/prometheus-fips:latest +prom/prometheus -> cgr.dev/chainguard/prometheus-iamguarded-fips:latest +prom/prometheus -> cgr.dev/chainguard/prometheus-iamguarded:latest +prom/prometheus -> cgr.dev/chainguard/prometheus:latest + +$ ./image-mapper map prom/prometheus --ignore-tiers=FIPS +prom/prometheus -> cgr.dev/chainguard/prometheus-iamguarded:latest +prom/prometheus -> cgr.dev/chainguard/prometheus:latest +``` + +### Ignore 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 map prom/prometheus --ignore-iamguarded +prom/prometheus -> cgr.dev/chainguard/prometheus-fips:latest +prom/prometheus -> cgr.dev/chainguard/prometheus:latest +``` diff --git a/image-mapper/docs/map_helm.md b/image-mapper/docs/map_helm.md new file mode 100644 index 0000000..9fa787a --- /dev/null +++ b/image-mapper/docs/map_helm.md @@ -0,0 +1,134 @@ +# Map Helm + +Streamline the process of migrating Helm charts to Chainguard by +extracting the image related values from a chart and mapping them to +Chainguard images. + +## Charts + +The `helm-chart` subcommand extracts all the image related values from a Helm +chart, as well as its dependencies and subcharts. + +``` +$ ./image-mapper map helm-chart argocd/argo-cd +redis-ha: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.2 # Original: 8.2.2-alpine + configmapTest: + image: + repository: cgr.dev/chainguard/shellcheck # Original: koalaman/shellcheck + tag: v0.11.0-dev # Original: v0.10.0 + haproxy: + image: + repository: cgr.dev/chainguard/haproxy # Original: ecr-public.aws.com/docker/library/haproxy + exporter: + image: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter +global: + image: + repository: cgr.dev/chainguard/argocd # Original: quay.io/argoproj/argocd +dex: + image: + repository: cgr.dev/chainguard/dex # Original: ghcr.io/dexidp/dex +redis: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.2 # Original: 8.2.2-alpine + exporter: + image: + repository: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter +server: + extensions: + image: + repository: cgr.dev/chainguard/argocd-extension-installer # Original: quay.io/argoprojlabs/argocd-extension-installer +``` + +This command doesn't require that `helm` is available locally but if you are +using a chart reference of the form `/` (i.e `argocd/argo-cd`) +then you must have added the repository with `helm repo add`. + +Alternatively, you can specify the chart repository and/or the chart version +directly. This will pull the chart directly, with no dependency on local +configuration at all. + +``` +$ ./image-mapper map helm-chart argo-cd \ + --chart-repo=https://argoproj.github.io/argo-helm \ + --chart-version=9.1.0 +``` + +## Values + +The `helm-values` subcommand extracts all the image related values from a values +file. + +This will only identify values that are explicitly listed in the file and may +not include all the values that are provided by subcharts or dependencies. + +``` +$ helm show values argocd/argo-cd | ./image-mapper map helm-values - +global: + image: + repository: cgr.dev/chainguard/argocd # Original: quay.io/argoproj/argocd +dex: + image: + repository: cgr.dev/chainguard/dex # Original: ghcr.io/dexidp/dex +redis: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.2 # Original: 8.2.2-alpine + exporter: + image: + repository: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter +redis-ha: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.2 # Original: 8.2.2-alpine + exporter: + image: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter + haproxy: + image: + repository: cgr.dev/chainguard/haproxy # Original: ecr-public.aws.com/docker/library/haproxy +server: + extensions: + image: + repository: cgr.dev/chainguard/argocd-extension-installer # Original: quay.io/argoprojlabs/argocd-extension-installer +``` + +## Options + +Both commands support a `--repository` flag which configures the repository +images are mapped to. This allows you to include your mirror or proxy URL in the +mappings. + +``` +$ ./image-mapper map helm-chart prometheus-community/kube-state-metrics --repository=registry.internal.mirror/cgr +image: + registry: registry.internal.mirror # Original: registry.k8s.io + repository: cgr/kube-state-metrics # Original: kube-state-metrics/kube-state-metrics +kubeRBACProxy: + image: + registry: registry.internal.mirror # Original: quay.io + repository: cgr/kube-rbac-proxy # Original: brancz/kube-rbac-proxy +``` + +## Testing + +You can validate whether the returned values have overridden all the images by +passing them to `helm template` and grepping for images. + +``` +$ ./image-mapper map helm-chart argocd/argo-cd \ + | helm template argocd/argo-cd -f - \ + | grep 'image:' + image: cgr.dev/chainguard/argocd:v3.2.1 + image: cgr.dev/chainguard/argocd:v3.2.1 + image: cgr.dev/chainguard/argocd:v3.2.1 + image: cgr.dev/chainguard/argocd:v3.2.1 + image: cgr.dev/chainguard/argocd:v3.2.1 + image: cgr.dev/chainguard/dex:v2.44.0 + image: cgr.dev/chainguard/argocd:v3.2.1 + image: cgr.dev/chainguard/redis:8.2.2 + image: cgr.dev/chainguard/argocd:v3.2.1 + image: cgr.dev/chainguard/argocd:v3.2.1 +``` diff --git a/image-mapper/go.mod b/image-mapper/go.mod index 9ce0e40..96331db 100644 --- a/image-mapper/go.mod +++ b/image-mapper/go.mod @@ -5,11 +5,119 @@ go 1.24.5 require ( github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.6 - github.com/spf13/cobra v1.9.1 + github.com/spf13/cobra v1.10.1 + gopkg.in/yaml.v3 v3.0.1 + helm.sh/helm/v3 v3.19.4 ) require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + 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/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/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 + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + 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/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-runewidth v0.0.9 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect + 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/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 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.18.0 // indirect + 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 + 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 + k8s.io/apiextensions-apiserver v0.34.2 // indirect + k8s.io/apimachinery v0.34.2 // indirect + k8s.io/apiserver v0.34.2 // indirect + k8s.io/cli-runtime v0.34.2 // indirect + k8s.io/client-go v0.34.2 // indirect + k8s.io/component-base v0.34.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/kubectl v0.34.2 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/image-mapper/go.sum b/image-mapper/go.sum index 4e154fe..a48a687 100644 --- a/image-mapper/go.sum +++ b/image-mapper/go.sum @@ -1,16 +1,451 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +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/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= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= +github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +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/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= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +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/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= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/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= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +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/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= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +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/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= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +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/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= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +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-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= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +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/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= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +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/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/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= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +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/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/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/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= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +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/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.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= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.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= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +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/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= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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= 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= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +helm.sh/helm/v3 v3.19.4 h1:E2yFBejmZBczWr5LblhjZbvAOAwVumfBO1AtN3nqI30= +helm.sh/helm/v3 v3.19.4/go.mod h1:PC1rk7PqacpkV4acUFMLStOOis7QM9Jq3DveHBInu4s= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= +k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= +k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= +k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= +k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= +k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= +k8s.io/cli-runtime v0.34.2 h1:cct1GEuWc3IyVT8MSCoIWzRGw9HJ/C5rgP32H60H6aE= +k8s.io/cli-runtime v0.34.2/go.mod h1:X13tsrYexYUCIq8MarCBy8lrm0k0weFPTpcaNo7lms4= +k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= +k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= +k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kubectl v0.34.2 h1:+fWGrVlDONMUmmQLDaGkQ9i91oszjjRAa94cr37hzqA= +k8s.io/kubectl v0.34.2/go.mod h1:X2KTOdtZZNrTWmUD4oHApJ836pevSl+zvC5sI6oO2YQ= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/image-mapper/internal/helm/chart.go b/image-mapper/internal/helm/chart.go new file mode 100644 index 0000000..374b53a --- /dev/null +++ b/image-mapper/internal/helm/chart.go @@ -0,0 +1,177 @@ +package helm + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/yamlhelpers" + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli" +) + +// ChartDescriptor describes a chart +type ChartDescriptor struct { + Name string + Repository string + Version string +} + +// MapChart extracts image related values from a Helm chart and maps them to +// Chainguard +func MapChart(ctx context.Context, chart ChartDescriptor, opts ...mapper.Option) ([]byte, error) { + // Create a temporary directory where we'll untar the chart + dir, err := os.MkdirTemp("", "") + if err != nil { + return nil, fmt.Errorf("creating temporary directory: %w", err) + } + defer os.RemoveAll(dir) + + // Pull the helm chart down to the temp dir + if err := helmPull(ctx, chart, dir); err != nil { + return nil, fmt.Errorf("pulling chart: %w", err) + } + + // Construct a mapper + m, err := NewMapper(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("constructing mapper: %w", err) + } + + // Map the images in the chart + return mapChart(m, dir) +} + +// mapChart extracts image related values from the chart and maps them to +// Chainguard +func mapChart(m mapper.Mapper, chartPath string) ([]byte, error) { + // Collect all the values.yaml files in the Chart and its subcharts + var valuesFiles []string + if err := filepath.WalkDir(chartPath, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + if !d.IsDir() { + return nil + } + + if _, err := os.Stat(filepath.Join(path, "Chart.yaml")); err != nil { + return nil + } + if _, err := os.Stat(filepath.Join(path, "values.yaml")); err != nil { + return nil + } + + valuesFiles = append(valuesFiles, filepath.Join(path, "values.yaml")) + + return nil + }); err != nil { + return nil, fmt.Errorf("walking chart directory: %w", err) + } + + // We'll write modified nodes to this node + outputNode := &yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{}, + } + + // Iterate backwards over the collected values files so that we map the + // child values before the parents, prefering any overrides configured + // in the parent values. + for i := len(valuesFiles) - 1; i >= 0; i-- { + path := valuesFiles[i] + + yamlPath := buildPath(strings.TrimPrefix(path, chartPath)) + + inputNode, err := readValuesFile(path) + if err != nil { + return nil, fmt.Errorf("reading values file: %s: %w", path, err) + } + + if err := yamlhelpers.WalkNode(inputNode, mapNode(m, yamlPath, outputNode)); err != nil { + return nil, err + } + + } + + // Marshal the modified nodes to a new document + doc := &yaml.Node{ + Kind: yaml.DocumentNode, + Content: []*yaml.Node{outputNode}, + } + output, err := yaml.Marshal(doc) + if err != nil { + return nil, fmt.Errorf("marshalling output document: %w", err) + } + + return output, nil +} + +// readValuesFile reads a values file from disk and returns it as a *yaml.Node +func readValuesFile(path string) (*yaml.Node, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + + var doc yaml.Node + if err := yaml.Unmarshal(data, &doc); err != nil { + return nil, fmt.Errorf("unmarshalling yaml: %w", err) + } + + // Return root mapping node (skip document wrapper) + if len(doc.Content) > 0 { + return doc.Content[0], nil + } + + return &yaml.Node{Kind: yaml.MappingNode}, nil +} + +// buildPath infers the appropriate nesting in the yaml structure based on the +// path to a values file. +// +// For instance, values in charts/grafana/values.yaml would be nested under +// "grafana". +// +// And values in charts/grafana/charts/redis/values.yaml would be nested under +// "grafana.redis". +func buildPath(path string) []string { + parent := []string{} + parts := strings.Split(filepath.Clean(path), string(filepath.Separator)) + for i, part := range parts { + if part != "charts" { + continue + } + + parent = append(parent, parts[i+1]) + } + + return parent +} + +// helmPull pulls a remote chart and extracts it to the specified directory +func helmPull(ctx context.Context, chart ChartDescriptor, dir string) error { + client := action.NewPullWithOpts(action.WithConfig(&action.Configuration{})) + client.Settings = cli.New() + client.DestDir = dir + client.Untar = true + + if chart.Version != "" { + client.Version = chart.Version + } + if chart.Repository != "" { + client.RepoURL = chart.Repository + } + + _, err := client.Run(chart.Name) + if err != nil { + return fmt.Errorf("pulling chart: %w", err) + } + + return nil +} diff --git a/image-mapper/internal/helm/chart_test.go b/image-mapper/internal/helm/chart_test.go new file mode 100644 index 0000000..3f7a354 --- /dev/null +++ b/image-mapper/internal/helm/chart_test.go @@ -0,0 +1,148 @@ +package helm + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestMapChart(t *testing.T) { + want := []byte(`test-subchart: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.1 # Original: 8.2.1-alpine + configmapTest: + image: + repository: cgr.dev/chainguard/shellcheck # Original: koalaman/shellcheck + tag: v0.11.0 # Original: v0.10.0 + haproxy: + image: + repository: cgr.dev/chainguard/haproxy # Original: ecr-public.aws.com/docker/library/haproxy + tag: 3.0.8 # Original: 3.0.8-alpine + sysctlImage: + registry: cgr.dev # Original: public.ecr.aws/docker/library + repository: chainguard/busybox # Original: busybox + exporter: + image: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter +global: + image: + repository: cgr.dev/chainguard/argocd # Original: quay.io/argoproj/argocd +dex: + image: + repository: cgr.dev/chainguard/dex # Original: ghcr.io/dexidp/dex +redis: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.2 # Original: 8.2.2-alpine + exporter: + image: + repository: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter +server: + extensions: + image: + repository: cgr.dev/chainguard/argocd-extension-installer # Original: quay.io/argoprojlabs/argocd-extension-installer +`) + + m := &mockMapper{ + mappings: map[string][]string{ + "ecr-public.aws.com/docker/library/haproxy:3.0.8-alpine": { + "cgr.dev/chainguard/haproxy:3.0.8", + }, + "ecr-public.aws.com/docker/library/redis:8.2.1-alpine": { + "cgr.dev/chainguard/redis:8.2.1", + }, + "ecr-public.aws.com/docker/library/redis:8.2.2-alpine": { + "cgr.dev/chainguard/redis:8.2.2", + }, + "ghcr.io/dexidp/dex:v2.44.0": { + "cgr.dev/chainguard/dex:v2.44.0", + }, + "ghcr.io/oliver006/redis_exporter:v1.75.0": { + "cgr.dev/chainguard/prometheus-redis-exporter:v1.75.0", + }, + "ghcr.io/oliver006/redis_exporter:v1.80.1": { + "cgr.dev/chainguard/prometheus-redis-exporter:v1.80.1", + }, + "koalaman/shellcheck:v0.10.0": { + "cgr.dev/chainguard/shellcheck:v0.11.0", + }, + "public.ecr.aws/docker/library/busybox:1.34.1": { + "cgr.dev/chainguard/busybox:1.34.1", + }, + "quay.io/argoproj/argocd": { + "cgr.dev/chainguard/argocd:latest", + }, + "quay.io/argoprojlabs/argocd-extension-installer:v0.0.9": { + "cgr.dev/chainguard/argocd-extension-installer:v0.0.9", + }, + }, + } + + got, err := mapChart(m, "testdata/test-chart") + if err != nil { + t.Fatalf("unexpected error mapping chart: %s", err) + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected values:\n%s", diff) + } +} + +func TestMapChartIntegration(t *testing.T) { + if v := os.Getenv("IMAGE_MAPPER_RUN_INTEGRATION_TESTS"); v == "" { + t.Skip() + } + + want := []byte(`test-subchart: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.1 # Original: 8.2.1-alpine + configmapTest: + image: + repository: cgr.dev/chainguard/shellcheck # Original: koalaman/shellcheck + tag: v0.11.0 # Original: v0.10.0 + haproxy: + image: + repository: cgr.dev/chainguard/haproxy # Original: ecr-public.aws.com/docker/library/haproxy + tag: 3.0.8-slim # Original: 3.0.8-alpine + sysctlImage: + registry: cgr.dev # Original: public.ecr.aws/docker/library + repository: chainguard/busybox # Original: busybox + tag: 1.36.0 # Original: 1.34.1 + exporter: + image: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter + tag: 1.75.0 # Original: v1.75.0 +global: + image: + repository: cgr.dev/chainguard/argocd # Original: quay.io/argoproj/argocd +dex: + image: + repository: cgr.dev/chainguard/dex # Original: ghcr.io/dexidp/dex +redis: + image: + repository: cgr.dev/chainguard/redis # Original: ecr-public.aws.com/docker/library/redis + tag: 8.2.2 # Original: 8.2.2-alpine + exporter: + image: + repository: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter +server: + extensions: + image: + repository: cgr.dev/chainguard/argocd-extension-installer # Original: quay.io/argoprojlabs/argocd-extension-installer +`) + + m, err := NewMapper(t.Context()) + if err != nil { + t.Fatalf("unexpected error constructing mapper: %s", err) + } + + got, err := mapChart(m, "testdata/test-chart") + if err != nil { + t.Fatalf("unexpected error mapping chart: %s", err) + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected values:\n%s", diff) + } +} diff --git a/image-mapper/internal/helm/mapper.go b/image-mapper/internal/helm/mapper.go new file mode 100644 index 0000000..8fcf62d --- /dev/null +++ b/image-mapper/internal/helm/mapper.go @@ -0,0 +1,39 @@ +package helm + +import ( + "context" + "strings" + + "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{ + // Helm charts are designed to work with + // specific versions. We include inactive tags + // here so we can match to the closest + // version. + mapper.WithInactiveTags(true), + mapper.WithIgnoreFns( + // Iamguarded images are designed to be + // used with our Helm charts. + mapper.IgnoreIamguarded(), + // TODO: make it possible select only + // FIPS images + mapper.IgnoreTiers([]string{"FIPS"}), + ), + // 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") + }, + ), + } + + return mapper.NewMapper(ctx, append(defaultOpts, opts...)...) +} diff --git a/image-mapper/internal/helm/testdata/test-chart/Chart.yaml b/image-mapper/internal/helm/testdata/test-chart/Chart.yaml new file mode 100644 index 0000000..7c90a89 --- /dev/null +++ b/image-mapper/internal/helm/testdata/test-chart/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +appVersion: 1.0.0 +dependencies: +- condition: test-subchart.enabled + name: test-subchart + repository: https://example.com/charts + version: 1.0.0 +description: A generic test Helm chart used for testing purposes +home: https://example.com +icon: https://example.com/icon.png +keywords: +- test +- example +kubeVersion: '>=1.25.0-0' +maintainers: +- name: example + url: https://example.com +name: test-chart +sources: +- https://example.com/test-chart +version: 1.0.0 diff --git a/image-mapper/internal/helm/testdata/test-chart/charts/test-subchart/Chart.yaml b/image-mapper/internal/helm/testdata/test-chart/charts/test-subchart/Chart.yaml new file mode 100644 index 0000000..d0c8310 --- /dev/null +++ b/image-mapper/internal/helm/testdata/test-chart/charts/test-subchart/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +appVersion: 1.0.0 +description: A generic test subchart used for testing purposes +home: https://example.com +icon: https://example.com/icon.png +keywords: +- test +- subchart +- example +maintainers: +- email: test@example.com + name: example +name: test-subchart +sources: +- https://example.com/test-subchart +version: 1.0.0 diff --git a/image-mapper/internal/helm/testdata/test-chart/charts/test-subchart/values.yaml b/image-mapper/internal/helm/testdata/test-chart/charts/test-subchart/values.yaml new file mode 100644 index 0000000..a158514 --- /dev/null +++ b/image-mapper/internal/helm/testdata/test-chart/charts/test-subchart/values.yaml @@ -0,0 +1,42 @@ +# Generic test subchart values + +# Main image configuration +image: + repository: ecr-public.aws.com/docker/library/redis + tag: 8.2.1-alpine + pullPolicy: IfNotPresent + +# Config validation test settings +configmapTest: + image: + repository: koalaman/shellcheck + tag: v0.10.0 + resources: {} + +# Proxy configuration +haproxy: + enabled: false + replicas: 3 + image: + repository: ecr-public.aws.com/docker/library/haproxy + tag: 3.0.8-alpine + pullPolicy: IfNotPresent + resources: {} + +# System control image +sysctlImage: + enabled: false + registry: public.ecr.aws/docker/library + repository: busybox + tag: 1.34.1 + pullPolicy: Always + resources: {} + +# Metrics exporter configuration +exporter: + enabled: false + image: ghcr.io/oliver006/redis_exporter + tag: v1.75.0 + pullPolicy: IfNotPresent + port: 9121 + resources: {} diff --git a/image-mapper/internal/helm/testdata/test-chart/values.yaml b/image-mapper/internal/helm/testdata/test-chart/values.yaml new file mode 100644 index 0000000..d887af2 --- /dev/null +++ b/image-mapper/internal/helm/testdata/test-chart/values.yaml @@ -0,0 +1,61 @@ +# Generic test chart values + +# Global configuration +global: + # Main application image + image: + repository: quay.io/argoproj/argocd + tag: "" + imagePullPolicy: IfNotPresent + +# Authentication service configuration +dex: + image: + repository: ghcr.io/dexidp/dex + tag: v2.44.0 + +# Cache/database configuration +redis: + image: + repository: ecr-public.aws.com/docker/library/redis + tag: 8.2.2-alpine + exporter: + image: + repository: ghcr.io/oliver006/redis_exporter + tag: v1.80.1 + +# Server configuration +server: + extensions: + image: + repository: quay.io/argoprojlabs/argocd-extension-installer + tag: v0.0.9 + +# Subchart configuration +test-subchart: + enabled: true + image: + repository: ecr-public.aws.com/docker/library/redis + tag: 8.2.1-alpine + pullPolicy: IfNotPresent + configmapTest: + image: + repository: koalaman/shellcheck + tag: v0.10.0 + haproxy: + enabled: false + image: + repository: ecr-public.aws.com/docker/library/haproxy + tag: 3.0.8-alpine + pullPolicy: IfNotPresent + sysctlImage: + enabled: false + registry: public.ecr.aws/docker/library + repository: busybox + tag: 1.34.1 + pullPolicy: Always + exporter: + enabled: false + image: ghcr.io/oliver006/redis_exporter + tag: v1.75.0 + pullPolicy: IfNotPresent diff --git a/image-mapper/internal/helm/values.go b/image-mapper/internal/helm/values.go new file mode 100644 index 0000000..29918c9 --- /dev/null +++ b/image-mapper/internal/helm/values.go @@ -0,0 +1,268 @@ +package helm + +import ( + "context" + "fmt" + + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/mapper" + "github.com/chainguard-dev/customer-success/scripts/image-mapper/internal/yamlhelpers" + "gopkg.in/yaml.v3" +) + +// MapValues extracts the image related values from a values file and maps them +// to Chainguard. +func MapValues(ctx context.Context, input []byte, opts ...mapper.Option) ([]byte, error) { + m, err := NewMapper(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("constructing the new mapper: %w", err) + } + + return mapValues(m, input) +} + +// mapValues extracts the image related values from a values file and maps them +// to Chainguard with the provided mapper +func mapValues(m mapper.Mapper, input []byte) ([]byte, error) { + var inputDoc yaml.Node + if err := yaml.Unmarshal(input, &inputDoc); err != nil { + return nil, fmt.Errorf("unmarshalling yaml: %w", err) + } + if len(inputDoc.Content) == 0 { + return nil, fmt.Errorf("provided input document is empty") + } + inputNode := inputDoc.Content[0] + + // We'll write modified nodes to this node + outputNode := &yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{}, + } + + // Walk the document recursively, adding image related fields to the + // output node and mapping them to Chainguard images + if err := yamlhelpers.WalkNode(inputNode, mapNode(m, []string{}, outputNode)); err != nil { + return nil, fmt.Errorf("walking nodes: %w", err) + } + + // Marshal the modified nodes to a new document + doc := &yaml.Node{ + Kind: yaml.DocumentNode, + Content: []*yaml.Node{outputNode}, + } + output, err := yaml.Marshal(doc) + if err != nil { + return nil, fmt.Errorf("marshalling output document: %w", err) + } + + return output, nil +} + +// mapNode returns a function that extracts image related fields from the input +// node and adds them to the output node, mapping the images to Chainguard where +// possible. +// +// It handles blocks like: +// +// image: +// repository: ghcr.io/foo/bar +// tag: v0.0.1 +// +// OR +// +// image: +// registry: ghcr.io +// repository: foo/bar +// tag: v0.0.1 +// +// OR +// +// image: +// name: ghcr.io/foo/bar +// tag: v0.0.1 +// +// OR +// +// image: ghcr.io/foo/bar:v0.0.1 +func mapNode(m mapper.Mapper, yamlPath []string, output *yaml.Node) yamlhelpers.WalkNodeFn { + return func(path []string, value *yaml.Node) error { + if value.Kind != yaml.MappingNode { + return nil + } + + // Extract all the keys from the map that are typically + // associated with an image + var ( + image *yaml.Node + name *yaml.Node + repository *yaml.Node + registry *yaml.Node + tag *yaml.Node + ) + for i := 0; i < len(value.Content); i += 2 { + key := value.Content[i].Value + value := value.Content[i+1] + + switch key { + case "image": + image = &yaml.Node{ + Kind: value.Kind, + Tag: value.Tag, + Value: value.Value, + } + case "name": + name = &yaml.Node{ + Kind: value.Kind, + Tag: value.Tag, + Value: value.Value, + } + case "repository": + repository = &yaml.Node{ + Kind: value.Kind, + Tag: value.Tag, + Value: value.Value, + } + case "registry": + registry = &yaml.Node{ + Kind: value.Kind, + Tag: value.Tag, + Value: value.Value, + } + case "tag": + tag = &yaml.Node{ + Kind: value.Kind, + Tag: value.Tag, + Value: value.Value, + } + } + } + + // If we don't have one of repository, name or image then we + // have no chance of figuring out the image mapping and we'll + // skip over it. + if !(hasValue(repository) || hasValue(name) || hasValue(image)) { + return nil + } + + // The key 'name' is too generic for us to assume it refers to + // an image, so ignore maps with keys called 'name' unless + // there are other signals that this is an image reference. + // + // For instance, if the map key is 'image', or we have a + // registry/tag alongside the name. + if hasValue(name) && !(path[len(path)-1] == "image" || registry != nil || tag != nil) { + return nil + } + + // Construct the image reference based on the fields + // available + img := "" + if hasValue(name) { + img = name.Value + } + if hasValue(image) { + img = image.Value + } + if hasValue(repository) { + img = repository.Value + } + if hasValue(registry) { + img = fmt.Sprintf("%s/%s", registry.Value, img) + } + if hasValue(tag) { + img = fmt.Sprintf("%s:%s", img, tag.Value) + } + + // Map the constructed image reference to the equivalent + // Chainguard image + mapping, err := mapper.MapImage(m, img) + if err == nil { + // Modify the values to follow the mapped image. This + // will ignore nodes that are nil. + setValue(repository, mapping.Context().String()) + setValue(image, mapping.Context().String()) + setValue(name, mapping.Context().String()) + setValue(registry, mapping.Context().RegistryStr()) + + // If there's no tag, then chances are image is a fully + // qualified image reference + if tag == nil { + setValue(image, mapping.String()) + } + + // If the registry key exists, then the + // repository shouldn't include the registry. + if registry != nil { + setValue(repository, mapping.Context().RepositoryStr()) + setValue(image, mapping.Context().RepositoryStr()) + setValue(name, mapping.Context().RepositoryStr()) + } + + // If the mapped tag is different to the tag in + // the original values, then replace it. + // + // Otherwise, leave it alone so that the output values + // don't include a specific tag have a better shot of + // being compatible across chart version upgrades. + if hasValue(tag) && tag.Value != mapping.Identifier() { + setValue(tag, mapping.Identifier()) + } + } + + // Create a new node and add all the modified values to it + node := &yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{}, + } + if err != nil { + node.HeadComment = fmt.Sprintf("Failed to map: %s: %s", img, err) + } + yamlhelpers.AddNode([]string{"registry"}, node, registry) + yamlhelpers.AddNode([]string{"image"}, node, image) + yamlhelpers.AddNode([]string{"name"}, node, name) + yamlhelpers.AddNode([]string{"repository"}, node, repository) + + // Only include the tag if we modified it + if tag != nil && tag.LineComment != "" { + yamlhelpers.AddNode([]string{"tag"}, node, tag) + } + + // Add the new node to the output values at the same path as the + // input + yamlhelpers.AddNode(append(yamlPath, path...), output, node) + + return nil + } +} + +// setValue sets the value of a scalar node +func setValue(node *yaml.Node, value string) { + if node == nil { + return + } + if node.Kind != yaml.ScalarNode { + return + } + + if node.LineComment == "" && node.Value != value { + node.LineComment = fmt.Sprintf("Original: %s", node.Value) + } + + node.Value = value + node.Tag = "!!str" +} + +// hasValue tells us whether a node has a value that we can try including in our +// mapping +func hasValue(node *yaml.Node) bool { + if node == nil { + return false + } + if node.Value == "" { + return false + } + if node.Tag == "!!null" { + return false + } + + return true +} diff --git a/image-mapper/internal/helm/values_test.go b/image-mapper/internal/helm/values_test.go new file mode 100644 index 0000000..fa8e7d1 --- /dev/null +++ b/image-mapper/internal/helm/values_test.go @@ -0,0 +1,139 @@ +package helm + +import ( + "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 TestMapValues(t *testing.T) { + input := []byte(` +prometheus: + image: prom/prometheus:v2.18.1 +redis-example: + exporter: + enabled: true + image: ghcr.io/oliver006/redis_exporter + tag: v1.75.0 + haproxy: + enabled: false + image: + repository: ecr-public.aws.com/docker/library/haproxy + image: + registry: ecr-public.aws.com + repository: docker/library/redis +proxy: + traefik: + image: + name: traefik + tag: v3.6.4 + +global: + revisionHistoryLimit: 3 + image: + repository: quay.io/argoproj/argocd + tag: "" + +prometheus-example: + admissionWebhooks: + deployment: + image: + registry: "quay.io" + repository: prometheus-operator/admission-webhook + patch: + image: + registry: "ghcr.io" + repository: jkroepke/kube-webhook-certgen + image: + registry: "" + repository: prometheus-operator/prometheus-operator + tag: "" + sha: "" +`) + + want := []byte(`prometheus: + image: cgr.dev/chainguard/prometheus:v2.56.0 # Original: prom/prometheus:v2.18.1 +redis-example: + exporter: + image: cgr.dev/chainguard/prometheus-redis-exporter # Original: ghcr.io/oliver006/redis_exporter + tag: v1.76.0 # Original: v1.75.0 + haproxy: + image: + repository: cgr.dev/chainguard/haproxy # Original: ecr-public.aws.com/docker/library/haproxy + image: + registry: cgr.dev # Original: ecr-public.aws.com + repository: chainguard/redis # Original: docker/library/redis +proxy: + traefik: + image: + name: cgr.dev/chainguard/traefik # Original: traefik +global: + image: + repository: cgr.dev/chainguard/argocd # Original: quay.io/argoproj/argocd +prometheus-example: + admissionWebhooks: + deployment: + image: + registry: cgr.dev # Original: quay.io + repository: chainguard/prometheus-admission-webhook # Original: prometheus-operator/admission-webhook + patch: + image: + registry: cgr.dev # Original: ghcr.io + repository: chainguard/kube-webhook-certgen # Original: jkroepke/kube-webhook-certgen + image: + registry: cgr.dev # Original: + repository: chainguard/prometheus-operator # Original: prometheus-operator/prometheus-operator +`) + + m := &mockMapper{ + mappings: map[string][]string{ + "ecr-public.aws.com/docker/library/haproxy": { + "cgr.dev/chainguard/haproxy:latest", + }, + "ecr-public.aws.com/docker/library/redis": { + "cgr.dev/chainguard/redis:latest", + }, + "ghcr.io/jkroepke/kube-webhook-certgen": { + "cgr.dev/chainguard/kube-webhook-certgen:latest", + }, + "ghcr.io/oliver006/redis_exporter:v1.75.0": { + "cgr.dev/chainguard/prometheus-redis-exporter:v1.76.0", + }, + "quay.io/argoproj/argocd": { + "cgr.dev/chainguard/argocd:latest", + }, + "quay.io/prometheus-operator/admission-webhook": { + "cgr.dev/chainguard/prometheus-admission-webhook:latest", + }, + "prom/prometheus:v2.18.1": { + "cgr.dev/chainguard/prometheus:v2.56.0", + }, + "prometheus-operator/prometheus-operator": { + "cgr.dev/chainguard/prometheus-operator:latest", + }, + "traefik:v3.6.4": { + "cgr.dev/chainguard/traefik:v3.6.4", + }, + }, + } + + got, err := mapValues(m, input) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected output:\n%s", diff) + } +} diff --git a/image-mapper/internal/mapper/filter_tag.go b/image-mapper/internal/mapper/filter_tag.go new file mode 100644 index 0000000..6499adf --- /dev/null +++ b/image-mapper/internal/mapper/filter_tag.go @@ -0,0 +1,32 @@ +package mapper + +// TagFilter is a function that filters tags +type TagFilter func(tag string) bool + +func includeTags(tags []string, filters ...TagFilter) []string { + if len(filters) == 0 { + return tags + } + var output []string + for _, tag := range tags { + if !includeTag(tag, filters...) { + continue + } + + output = append(output, tag) + } + + return output +} + +func includeTag(tag string, filters ...TagFilter) bool { + for _, filter := range filters { + if !filter(tag) { + continue + } + + return true + } + + return false +} diff --git a/image-mapper/internal/mapper/filter_tag_test.go b/image-mapper/internal/mapper/filter_tag_test.go new file mode 100644 index 0000000..46456cc --- /dev/null +++ b/image-mapper/internal/mapper/filter_tag_test.go @@ -0,0 +1,116 @@ +package mapper + +import ( + "reflect" + "strings" + "testing" +) + +func TestIncludeTags(t *testing.T) { + tags := []string{"latest", "v1.0.0", "v2.0.0", "dev", "prod", "staging"} + + tests := []struct { + name string + tags []string + filters []TagFilter + expected []string + }{ + { + name: "no filters returns all tags", + tags: tags, + filters: nil, + expected: tags, + }, + { + name: "empty filters returns all tags", + tags: tags, + filters: []TagFilter{}, + expected: tags, + }, + { + 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: "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: "no tags match filters", + tags: tags, + filters: []TagFilter{ + func(tag string) bool { return strings.HasPrefix(tag, "nonexistent") }, + }, + expected: nil, + }, + { + name: "empty tags slice", + tags: []string{}, + filters: []TagFilter{ + func(tag string) bool { return true }, + }, + expected: nil, + }, + { + name: "filter returns true for all", + tags: tags, + filters: []TagFilter{ + func(tag string) bool { return true }, + }, + expected: tags, + }, + { + name: "filter returns false for all", + tags: tags, + filters: []TagFilter{ + func(tag string) bool { return false }, + }, + 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: "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: "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") }, + }, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := includeTags(tt.tags, tt.filters...) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("includeTags() = %v, expected %v", result, tt.expected) + } + }) + } +} diff --git a/image-mapper/internal/mapper/mapper.go b/image-mapper/internal/mapper/mapper.go index fd92e0d..3a7ca38 100644 --- a/image-mapper/internal/mapper/mapper.go +++ b/image-mapper/internal/mapper/mapper.go @@ -16,35 +16,48 @@ type Mapping struct { } // Mapper maps image references to images in our catalog -type Mapper struct { - repos []Repo - ignoreFns []IgnoreFn - repoName string +type Mapper interface { + Map(image string) (*Mapping, error) +} + +type mapper struct { + repos []Repo + ignoreFns []IgnoreFn + includeTags []TagFilter + repoName string } // NewMapper creates a new mapper -func NewMapper(ctx context.Context, opts ...Option) (*Mapper, error) { - o := &options{} +func NewMapper(ctx context.Context, opts ...Option) (*mapper, error) { + o := &options{ + repo: "cgr.dev/chainguard", + } for _, opt := range opts { opt(o) } - repos, err := listRepos(ctx) + repoName, err := parseRepo(o.repo) + if err != nil { + return nil, fmt.Errorf("parsing repository: %w", err) + } + + repos, err := listRepos(ctx, o.inactiveTags) if err != nil { return nil, fmt.Errorf("listing repos: %w", err) } - m := &Mapper{ - repos: repos, - ignoreFns: o.ignoreFns, - repoName: "cgr.dev/chainguard", + m := &mapper{ + repos: repos, + ignoreFns: o.ignoreFns, + includeTags: o.includeTags, + repoName: repoName, } return m, nil } // MapAll returns mappings for all the images returned by the iterator -func (m *Mapper) MapAll(it Iterator) ([]*Mapping, error) { +func (m *mapper) MapAll(it Iterator) ([]*Mapping, error) { mapped := make(map[string]struct{}) mappings := []*Mapping{} for { @@ -73,7 +86,7 @@ func (m *Mapper) MapAll(it Iterator) ([]*Mapping, error) { } // Map an upstream image to the corresponding images in chainguard-private -func (m *Mapper) Map(image string) (*Mapping, error) { +func (m *mapper) Map(image string) (*Mapping, error) { ref, err := name.NewTag(strings.Split(image, "@")[0]) if err != nil { return nil, fmt.Errorf("parsing %s: %w", image, err) @@ -101,14 +114,21 @@ func (m *Mapper) Map(image string) (*Mapping, error) { } // Format the matches into the results we'll include in the mappings - results := []string{} for _, cgrrepo := range matches { // Append the repository name to the rest of the reference result := fmt.Sprintf("%s/%s", m.repoName, cgrrepo.Name) - // Try and match the provided tag to one of the active tags - tag := MatchTag(cgrrepo.ActiveTags, ref.TagStr()) + // 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...) + + // Try and match the provided tag to one of the tags + tag := MatchTag(tags, ref.TagStr()) if tag != "" { result = fmt.Sprintf("%s:%s", result, tag) } @@ -122,7 +142,7 @@ func (m *Mapper) Map(image string) (*Mapping, error) { }, nil } -func (m *Mapper) ignoreRepo(repo Repo) bool { +func (m *mapper) ignoreRepo(repo Repo) bool { for _, ignore := range m.ignoreFns { if !ignore(repo) { continue @@ -132,3 +152,23 @@ func (m *Mapper) ignoreRepo(repo Repo) bool { return false } + +// MapImage maps the provided image to its Chainguard equivalent. It returns the +// first result it finds. +func MapImage(m Mapper, img string) (name.Reference, error) { + mapping, err := m.Map(img) + if err != nil { + return nil, fmt.Errorf("mapping image: %s: %w", img, err) + } + if len(mapping.Results) == 0 { + return nil, fmt.Errorf("no results found") + } + result := mapping.Results[0] + + mapped, err := name.NewTag(result) + if err != nil { + return nil, fmt.Errorf("parsing mapped image: %w", err) + } + + return mapped, nil +} diff --git a/image-mapper/internal/mapper/mapper_test.go b/image-mapper/internal/mapper/mapper_test.go index 25ce124..41e5d1a 100644 --- a/image-mapper/internal/mapper/mapper_test.go +++ b/image-mapper/internal/mapper/mapper_test.go @@ -91,7 +91,7 @@ func TestMapperMap(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - m := &Mapper{ + m := &mapper{ repos: tc.repos, repoName: "cgr.dev/chainguard", ignoreFns: []IgnoreFn{IgnoreTiers([]string{"fips"})}, @@ -115,7 +115,7 @@ func TestMapperMap(t *testing.T) { } func TestMapperMapInvalidImage(t *testing.T) { - m := &Mapper{ + m := &mapper{ repos: []Repo{}, } @@ -139,7 +139,7 @@ func TestMapperMapAll(t *testing.T) { }, } - m := &Mapper{ + m := &mapper{ repos: repos, repoName: "cgr.dev/chainguard", } @@ -186,7 +186,7 @@ func TestMapperMapAllDuplicates(t *testing.T) { }, } - m := &Mapper{ + m := &mapper{ repos: repos, repoName: "cgr.dev/chainguard", } @@ -227,7 +227,7 @@ func TestMapperMapAllDuplicates(t *testing.T) { } func TestMapperMapAllIteratorError(t *testing.T) { - m := &Mapper{ + m := &mapper{ repos: []Repo{}, } expectedErr := errors.New("iterator error") @@ -240,7 +240,7 @@ func TestMapperMapAllIteratorError(t *testing.T) { } func TestMapperMapAllMapError(t *testing.T) { - m := &Mapper{ + m := &mapper{ repos: []Repo{}, } @@ -494,7 +494,7 @@ func TestMapperMapWithCustomIgnoreFn(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - m := &Mapper{ + m := &mapper{ repos: tc.repos, repoName: "cgr.dev/chainguard", ignoreFns: tc.ignoreFns, @@ -545,7 +545,7 @@ func TestMapperMapWithCustomIgnoreFnUsingAliases(t *testing.T) { return false } - m := &Mapper{ + m := &mapper{ repos: repos, repoName: "cgr.dev/chainguard", ignoreFns: []IgnoreFn{ignoreFn}, @@ -601,7 +601,7 @@ func TestMapperMapWithNoIgnoreFns(t *testing.T) { }, } - m := &Mapper{ + m := &mapper{ repos: repos, repoName: "cgr.dev/chainguard", ignoreFns: []IgnoreFn{}, // No ignore functions @@ -627,6 +627,128 @@ func TestMapperMapWithNoIgnoreFns(t *testing.T) { } } +func TestMapImage(t *testing.T) { + testCases := []struct { + name string + image string + repos []Repo + expectedImage string + expectError bool + }{ + { + name: "successful mapping with result", + image: "nginx", + repos: []Repo{ + { + Name: "nginx", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + }, + expectedImage: "cgr.dev/chainguard/nginx", + expectError: false, + }, + { + name: "no results found", + image: "nonexistent", + repos: []Repo{ + { + Name: "redis", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + }, + expectError: true, + }, + { + name: "multiple results returns first", + image: "nginx", + repos: []Repo{ + { + Name: "nginx", + CatalogTier: "APPLICATION", + Aliases: []string{}, + }, + { + Name: "nginx-custom", + CatalogTier: "APPLICATION", + Aliases: []string{"nginx"}, + }, + }, + expectedImage: "cgr.dev/chainguard/nginx", + expectError: false, + }, + { + name: "image with tag", + image: "nginx:1.25", + repos: []Repo{ + { + Name: "nginx", + CatalogTier: "APPLICATION", + ActiveTags: []string{"latest", "1.25", "1.26"}, + Aliases: []string{}, + }, + }, + expectedImage: "cgr.dev/chainguard/nginx:1.25", + expectError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := &mapper{ + repos: tc.repos, + repoName: "cgr.dev/chainguard", + } + + result, err := MapImage(m, tc.image) + + if tc.expectError { + if err == nil { + t.Error("expected error but got none") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result.String() != tc.expectedImage { + t.Errorf("expected %s, got %s", tc.expectedImage, result.String()) + } + }) + } +} + +func TestMapImageInvalidImage(t *testing.T) { + m := &mapper{ + repos: []Repo{}, + } + + _, err := MapImage(m, "invalid::image") + if err == nil { + t.Error("expected error for invalid image reference") + } +} + +func TestMapImageMapperError(t *testing.T) { + m := &errorMapper{err: errors.New("mapper error")} + + _, err := MapImage(m, "nginx") + if err == nil { + t.Error("expected error from mapper") + } +} + +type errorMapper struct { + err error +} + +func (m *errorMapper) Map(image string) (*Mapping, error) { + return nil, m.err +} + func TestMapperIntegration(t *testing.T) { if v := os.Getenv("IMAGE_MAPPER_RUN_INTEGRATION_TESTS"); v == "" { t.Skip() @@ -679,6 +801,10 @@ func TestMapperIntegration(t *testing.T) { "cgr.dev/chainguard/crossplane-aws-eks", "cgr.dev/chainguard/crossplane-aws-eks-fips", }, + "ghcr.io/crossplane-contrib/provider-aws-elasticache:v1.20.1": { + "cgr.dev/chainguard/crossplane-aws-elasticache", + "cgr.dev/chainguard/crossplane-aws-elasticache-fips", + }, "ghcr.io/crossplane-contrib/provider-aws-firehose:v1.20.1": { "cgr.dev/chainguard/crossplane-aws-firehose", "cgr.dev/chainguard/crossplane-aws-firehose-fips", @@ -699,6 +825,10 @@ func TestMapperIntegration(t *testing.T) { "cgr.dev/chainguard/crossplane-aws-lambda", "cgr.dev/chainguard/crossplane-aws-lambda-fips", }, + "ghcr.io/crossplane-contrib/provider-aws-memorydb:v1.20.1": { + "cgr.dev/chainguard/crossplane-aws-memorydb", + "cgr.dev/chainguard/crossplane-aws-memorydb-fips", + }, "ghcr.io/crossplane-contrib/provider-aws-rds:v1.20.1": { "cgr.dev/chainguard/crossplane-aws-rds", "cgr.dev/chainguard/crossplane-aws-rds-fips", @@ -799,8 +929,6 @@ func TestMapperIntegration(t *testing.T) { "cgr.dev/chainguard/argocd-fips", "cgr.dev/chainguard/argocd-iamguarded", "cgr.dev/chainguard/argocd-iamguarded-fips", - "cgr.dev/chainguard/argocd-repo-server", - "cgr.dev/chainguard/argocd-repo-server-fips", }, "quay.io/argoproj/argocli:latest": { "cgr.dev/chainguard/argo-cli", diff --git a/image-mapper/internal/mapper/match_tag.go b/image-mapper/internal/mapper/match_tag.go index 8edf37c..5d3fde4 100644 --- a/image-mapper/internal/mapper/match_tag.go +++ b/image-mapper/internal/mapper/match_tag.go @@ -5,11 +5,11 @@ import ( "strconv" ) -// MatchTag returns the best matching active tag for the input tag. It'll return +// MatchTag returns the best matching tag for the input tag. It'll return // an empty string if it can't find an appropriate match. -func MatchTag(activeTags []string, tag string) string { +func MatchTag(tags []string, tag string) string { for _, fn := range matchTagFns { - match := fn(activeTags, tag) + match := fn(tags, tag) if match == "" { continue } @@ -20,8 +20,8 @@ func MatchTag(activeTags []string, tag string) string { return "" } -// MatchTagFn matches a tag to one of the provided active tags -type MatchTagFn func(activeTag []string, tag string) string +// MatchTagFn matches a tag to one of the provided tags +type MatchTagFn func(tags []string, tag string) string var matchTagFns = []MatchTagFn{ matchEqualTag, @@ -29,10 +29,10 @@ var matchTagFns = []MatchTagFn{ } // matchEqualTag identifies an exact match between the input tag and one of the -// activeTags -func matchEqualTag(activeTags []string, tag string) string { - for _, activeTag := range activeTags { - if activeTag != tag { +// tags +func matchEqualTag(tags []string, tag string) string { + for _, t := range tags { + if t != tag { continue } return tag @@ -49,7 +49,7 @@ func matchEqualTag(activeTags []string, tag string) string { // 2 -> 3 // 3.7 -> 3.9 // 3.11.1 -> 3.11.5 -func matchClosestSemanticVersionTag(activeTags []string, tag string) string { +func matchClosestSemanticVersionTag(tags []string, tag string) string { parsedTag := parseTag(tag) if parsedTag == nil { return "" @@ -60,34 +60,39 @@ func matchClosestSemanticVersionTag(activeTags []string, tag string) string { bestMatchStr string ) - for _, activeTag := range activeTags { - parsedActive := parseTag(activeTag) - if parsedActive == nil { + for _, t := range tags { + parsedT := parseTag(t) + if parsedT == nil { continue } // Must have same specificity (i.e major, minor, patch) - if parsedActive.specificity != parsedTag.specificity { + if parsedT.specificity != parsedTag.specificity { continue } - // Must both have v prefix, or no v prefix - if parsedActive.hasV != parsedTag.hasV { + // Tag must be >= input tag + if parsedT.LessThan(parsedTag) { continue } - // Active tag must be >= input tag - if parsedActive.LessThan(parsedTag) { - continue - } - - // Active tag must be < the current best match we've found - if bestMatch != nil && !parsedActive.LessThan(bestMatch) { - continue + // Compare with current best match + if bestMatch != nil { + // Naturally, a larger version is a worse match + if parsedT.GreaterThan(bestMatch) { + continue + } + + // For equal matches, prefer tags with the same format. + // For instance, if the current best match for v1.2.3 is + // 1.2.4, we should prefer v1.2.4. + if parsedT.Equals(bestMatch) && !(parsedT.hasV == parsedTag.hasV && bestMatch.hasV != parsedTag.hasV) { + continue + } } - bestMatch = parsedActive - bestMatchStr = activeTag + bestMatch = parsedT + bestMatchStr = t } @@ -150,9 +155,19 @@ func parseTag(tag string) *tagVersion { return tv } +// Equals tests whether this tag is equal to the provided one +func (tv *tagVersion) Equals(other *tagVersion) bool { + return tv.compare(other) == 0 +} + // LessThan tests whether this tag is less than the provided one func (tv *tagVersion) LessThan(other *tagVersion) bool { - return other != nil && tv.compare(other) < 0 + return tv.compare(other) < 0 +} + +// GreaterThan tests whether this tag is greater than the provided one +func (tv *tagVersion) GreaterThan(other *tagVersion) bool { + return tv.compare(other) > 0 } // compare returns -1 if tv < other, 0 if equal, 1 if tv > other diff --git a/image-mapper/internal/mapper/match_tag_test.go b/image-mapper/internal/mapper/match_tag_test.go index 3e89aa8..ffac1df 100644 --- a/image-mapper/internal/mapper/match_tag_test.go +++ b/image-mapper/internal/mapper/match_tag_test.go @@ -10,6 +10,7 @@ func TestMatchTag(t *testing.T) { "latest-dev", "3", "3.14", + "3.14.1", "3.14.2", "3.13", "3.13.6", @@ -42,8 +43,13 @@ func TestMatchTag(t *testing.T) { }, { name: "nearest higher patch in same minor", - tag: "3.14.1", - expected: "3.14.2", + tag: "3.14.0", + expected: "3.14.1", + }, + { + name: "cross-prefix match prefers better semantic version", + tag: "v3.14.1", + expected: "3.14.1", }, { name: "nearest higher patch in next minor", @@ -76,9 +82,9 @@ func TestMatchTag(t *testing.T) { expected: "v3", }, { - name: "v-prefix nearest higher patch", + name: "v-prefix cross-prefix match to exact version", tag: "v3.14.1", - expected: "v3.14.2", + expected: "3.14.1", }, { name: "v-prefix nearest higher minor", @@ -152,8 +158,13 @@ func TestMatchTag(t *testing.T) { }, { name: "suffix nearest higher patch in same minor", - tag: "3.14.1-alpine", - expected: "3.14.2", + tag: "3.14.0-alpine", + expected: "3.14.1", + }, + { + name: "suffix cross-prefix match prefers better semantic version", + tag: "v3.14.1-alpine", + expected: "3.14.1", }, { name: "suffix nearest higher patch in next minor", @@ -171,9 +182,9 @@ func TestMatchTag(t *testing.T) { expected: "3", }, { - name: "suffix v-prefix nearest higher patch", + name: "suffix v-prefix cross-prefix match to exact version", tag: "v3.14.1-alpine", - expected: "v3.14.2", + expected: "3.14.1", }, { name: "suffix v-prefix nearest higher minor", diff --git a/image-mapper/internal/mapper/options.go b/image-mapper/internal/mapper/options.go index cd82d54..3a56797 100644 --- a/image-mapper/internal/mapper/options.go +++ b/image-mapper/internal/mapper/options.go @@ -4,7 +4,10 @@ package mapper type Option func(*options) type options struct { - ignoreFns []IgnoreFn + ignoreFns []IgnoreFn + repo string + inactiveTags bool + includeTags []TagFilter } // WithIgnoreFns is a functional option that configures the IgnoreFns used by @@ -14,3 +17,27 @@ func WithIgnoreFns(ignoreFns ...IgnoreFn) Option { o.ignoreFns = ignoreFns } } + +// WithRepository is a functional option that configures the repository prefix +// of the returned results +func WithRepository(repo string) Option { + return func(o *options) { + o.repo = repo + } +} + +// WithIncludeTags is a functional option that configures filters that define +// which tags to include when matching tags +func WithIncludeTags(includeTags ...TagFilter) Option { + return func(o *options) { + o.includeTags = includeTags + } +} + +// WithInactiveTags is a functional option that configures the mapper to include +// inactive tags in its matching +func WithInactiveTags(inactiveTags bool) Option { + return func(o *options) { + o.inactiveTags = inactiveTags + } +} diff --git a/image-mapper/internal/mapper/repos.go b/image-mapper/internal/mapper/repos.go index 713fbe6..e4734e6 100644 --- a/image-mapper/internal/mapper/repos.go +++ b/image-mapper/internal/mapper/repos.go @@ -6,6 +6,8 @@ import ( "encoding/json" "fmt" "net/http" + + "github.com/google/go-containerregistry/pkg/name" ) // Repo describes a repo in the catalog @@ -14,13 +16,82 @@ type Repo struct { CatalogTier string `json:"catalogTier"` Aliases []string `json:"aliases"` ActiveTags []string `json:"activeTags"` + Tags []Tag `json:"tags"` +} + +// Tag is a tag in a repository +type Tag struct { + Name string `json:"name"` +} + +func flattenTags(tags []Tag) []string { + var flattened []string + for _, tag := range tags { + flattened = append(flattened, tag.Name) + } + + return flattened +} + +// parseRepo parses a 'repository' as expressed as a registry hostname +// (foo.bar.com) or a repository name (foo.bar.com/foo/bar). +func parseRepo(repo string) (string, error) { + if ref, err := name.NewRegistry(repo); err == nil { + return ref.String(), nil + } + + if ref, err := name.NewRepository(repo); err == nil { + return ref.String(), nil + } + + return "", fmt.Errorf("can't parse repository: %s", repo) +} + +var ( + repoQuery = ` +query ChainguardPrivateImageCatalog { + repos(filter: {uidp: {childrenOf: "ce2d1984a010471142503340d670612d63ffb9f6"}}) { + name + aliases + catalogTier + activeTags + } } +` -func listRepos(ctx context.Context) ([]Repo, error) { + repoQueryWithTags = ` +query ChainguardPrivateImageCatalog { + repos(filter: {uidp: {childrenOf: "ce2d1984a010471142503340d670612d63ffb9f6"}}) { + name + aliases + catalogTier + activeTags + tags(filter: {excludeDates: true, excludeEpochs: true, excludeReferrers: true}) { + name + } + } +} +` +) + +func listRepos(ctx context.Context, inactiveTags bool) ([]Repo, error) { c := &http.Client{} - buf := bytes.NewReader([]byte(`{"query":"query OrganizationImageCatalog($organization: ID!) {\n repos(filter: {uidp: {childrenOf: $organization}}) {\n name\n aliases\n catalogTier\n activeTags\n }\n}","variables":{"excludeDates":true,"excludeEpochs":true,"organization":"ce2d1984a010471142503340d670612d63ffb9f6"}}`)) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://data.chainguard.dev/query?id=PrivateImageCatalog", buf) + body := struct { + Query string `json:"query"` + }{ + Query: repoQuery, + } + if inactiveTags { + body.Query = repoQueryWithTags + } + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(body); err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://data.chainguard.dev/query", &buf) if err != nil { return nil, fmt.Errorf("constructing request: %w", err) } @@ -70,6 +141,8 @@ func fixAliases(repos []Repo) []Repo { } var aliasesFixes = map[string][]string{ + "argocd-repo-server": {}, + "argocd-repo-server-fips": {}, "argo-cli": { "quay.io/argoproj/argocli", }, @@ -133,6 +206,12 @@ var aliasesFixes = map[string][]string{ "crossplane-aws-eks-fips": { "ghcr.io/crossplane-contrib/provider-aws-eks", }, + "crossplane-aws-elasticache": { + "ghcr.io/crossplane-contrib/provider-aws-elasticache", + }, + "crossplane-aws-elasticache-fips": { + "ghcr.io/crossplane-contrib/provider-aws-elasticache", + }, "crossplane-aws-fips": { "ghcr.io/crossplane-contrib/provider-family-aws", }, @@ -166,6 +245,12 @@ var aliasesFixes = map[string][]string{ "crossplane-aws-lambda-fips": { "ghcr.io/crossplane-contrib/provider-aws-lambda", }, + "crossplane-aws-memorydb": { + "ghcr.io/crossplane-contrib/provider-aws-memorydb", + }, + "crossplane-aws-memorydb-fips": { + "ghcr.io/crossplane-contrib/provider-aws-memorydb", + }, "crossplane-aws-rds": { "ghcr.io/crossplane-contrib/provider-aws-rds", }, @@ -280,6 +365,42 @@ var aliasesFixes = map[string][]string{ "flux-source-controller-fips": { "ghcr.io/fluxcd/source-controller", }, + "kyverno-cli": { + "ghcr.io/kyverno/kyverno-cli", + }, + "kyverno-cli-fips": { + "ghcr.io/kyverno/kyverno-cli-fips", + }, + "kyverno": { + "ghcr.io/kyverno/kyverno", + }, + "kyverno-fips": { + "ghcr.io/kyverno/kyverno", + }, + "kyvernopre": { + "ghcr.io/kyverno/kyvernopre", + }, + "kyvernopre-fips": { + "ghcr.io/kyverno/kyvernopre", + }, + "kyverno-background-controller": { + "ghcr.io/kyverno/background-controller", + }, + "kyverno-background-controller-fips": { + "ghcr.io/kyverno/background-controller", + }, + "kyverno-cleanup-controller": { + "ghcr.io/kyverno/cleanup-controller", + }, + "kyverno-cleanup-controller-fips": { + "ghcr.io/kyverno/cleanup-controller", + }, + "kyverno-reports-controller": { + "ghcr.io/kyverno/reports-controller", + }, + "kyverno-reports-controller-fips": { + "ghcr.io/kyverno/reports-controller", + }, "minio-client": { "quay.io/minio/mc", }, diff --git a/image-mapper/internal/yamlhelpers/add.go b/image-mapper/internal/yamlhelpers/add.go new file mode 100644 index 0000000..0f4fbe2 --- /dev/null +++ b/image-mapper/internal/yamlhelpers/add.go @@ -0,0 +1,56 @@ +package yamlhelpers + +import "gopkg.in/yaml.v3" + +// AddNode adds a node at the specified path +func AddNode(path []string, node *yaml.Node, add *yaml.Node) { + if add == nil { + return + } + + current := node + + for i, key := range path { + // If this is the last element of the path then add the node or + // replace the existing node + if i == len(path)-1 { + // Check if key already exists and replace its value + for j := 0; j < len(current.Content); j += 2 { + if current.Content[j].Value == key { + // Replace the existing value + current.Content[j+1] = add + return + } + } + // Key doesn't exist, add new key-value pair + current.Content = append(current.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: key}, + add, + ) + return + } + + // Otherwise, create the path (if it doesn't already exist) + var next *yaml.Node + for j := 0; j < len(current.Content); j += 2 { + if current.Content[j].Value != key { + continue + } + next = current.Content[j+1] + break + } + if next == nil { + // Create new intermediate mapping + next = &yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{}, + } + current.Content = append(current.Content, + &yaml.Node{Kind: yaml.ScalarNode, Value: key}, + next, + ) + } + + current = next + } +} diff --git a/image-mapper/internal/yamlhelpers/add_test.go b/image-mapper/internal/yamlhelpers/add_test.go new file mode 100644 index 0000000..7d2deeb --- /dev/null +++ b/image-mapper/internal/yamlhelpers/add_test.go @@ -0,0 +1,378 @@ +package yamlhelpers + +import ( + "testing" + + "gopkg.in/yaml.v3" +) + +func TestAddNode(t *testing.T) { + testCases := []struct { + name string + initial string + path []string + addValue string + expected string + }{ + { + name: "add to empty mapping", + initial: `{}`, + path: []string{"newkey"}, + addValue: "newvalue", + expected: `newkey: newvalue +`, + }, + { + name: "add new key to existing mapping", + initial: `existing: value +`, + path: []string{"newkey"}, + addValue: "newvalue", + expected: `existing: value +newkey: newvalue +`, + }, + { + name: "replace existing key", + initial: `key: oldvalue +`, + path: []string{"key"}, + addValue: "newvalue", + expected: `key: newvalue +`, + }, + { + name: "create nested path", + initial: `{}`, + path: []string{"level1", "level2", "level3"}, + addValue: "deepvalue", + expected: `level1: + level2: + level3: deepvalue +`, + }, + { + name: "add to existing nested path", + initial: `level1: + existing: value +`, + path: []string{"level1", "newkey"}, + addValue: "newvalue", + expected: `level1: + existing: value + newkey: newvalue +`, + }, + { + name: "replace in nested path", + initial: `level1: + level2: + key: oldvalue +`, + path: []string{"level1", "level2", "key"}, + addValue: "newvalue", + expected: `level1: + level2: + key: newvalue +`, + }, + { + name: "add sibling to nested structure", + initial: `parent: + child1: value1 +`, + path: []string{"parent", "child2"}, + addValue: "value2", + expected: `parent: + child1: value1 + child2: value2 +`, + }, + { + name: "create intermediate paths", + initial: `existing: value +`, + path: []string{"new", "nested", "key"}, + addValue: "value", + expected: `existing: value +new: + nested: + key: value +`, + }, + { + name: "add to root with multiple existing keys", + initial: `key1: value1 +key2: value2 +key3: value3 +`, + path: []string{"key4"}, + addValue: "value4", + expected: `key1: value1 +key2: value2 +key3: value3 +key4: value4 +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var node yaml.Node + err := yaml.Unmarshal([]byte(tc.initial), &node) + if err != nil { + t.Fatalf("failed to unmarshal initial yaml: %v", err) + } + + addNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: tc.addValue, + } + + // The root node is a DocumentNode, we need to work with its first child (the MappingNode) + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + AddNode(tc.path, node.Content[0], addNode) + } else { + t.Fatal("unexpected node structure") + } + + out, err := yaml.Marshal(&node) + if err != nil { + t.Fatalf("failed to marshal result: %v", err) + } + + // Unmarshal both to compare semantically rather than as strings + var gotMap, expectedMap interface{} + if err := yaml.Unmarshal(out, &gotMap); err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + if err := yaml.Unmarshal([]byte(tc.expected), &expectedMap); err != nil { + t.Fatalf("failed to unmarshal expected: %v", err) + } + + // Compare the semantic content + gotYAML, _ := yaml.Marshal(gotMap) + expectedYAML, _ := yaml.Marshal(expectedMap) + if string(gotYAML) != string(expectedYAML) { + t.Errorf("expected:\n%s\ngot:\n%s", string(expectedYAML), string(gotYAML)) + } + }) + } +} + +func TestAddNodeNil(t *testing.T) { + initial := `key: value +` + + var node yaml.Node + err := yaml.Unmarshal([]byte(initial), &node) + if err != nil { + t.Fatalf("failed to unmarshal yaml: %v", err) + } + + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + AddNode([]string{"newkey"}, node.Content[0], nil) + } + + out, err := yaml.Marshal(&node) + if err != nil { + t.Fatalf("failed to marshal result: %v", err) + } + + if string(out) != initial { + t.Errorf("expected node to be unchanged when adding nil, got:\n%s", string(out)) + } +} + +func TestAddNodeComplexValue(t *testing.T) { + testCases := []struct { + name string + initial string + path []string + addNode *yaml.Node + expected string + }{ + { + name: "add mapping node", + initial: `{}`, + path: []string{"config"}, + addNode: &yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "host"}, + {Kind: yaml.ScalarNode, Value: "localhost"}, + {Kind: yaml.ScalarNode, Value: "port"}, + {Kind: yaml.ScalarNode, Value: "8080"}, + }, + }, + expected: `config: + host: localhost + port: 8080 +`, + }, + { + name: "add sequence node", + initial: `{}`, + path: []string{"items"}, + addNode: &yaml.Node{ + Kind: yaml.SequenceNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "item1"}, + {Kind: yaml.ScalarNode, Value: "item2"}, + {Kind: yaml.ScalarNode, Value: "item3"}, + }, + }, + expected: `items: + - item1 + - item2 + - item3 +`, + }, + { + name: "replace with complex node", + initial: `simple: value +`, + path: []string{"simple"}, + addNode: &yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Value: "nested"}, + {Kind: yaml.ScalarNode, Value: "value"}, + }, + }, + expected: `simple: + nested: value +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var node yaml.Node + err := yaml.Unmarshal([]byte(tc.initial), &node) + if err != nil { + t.Fatalf("failed to unmarshal initial yaml: %v", err) + } + + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + AddNode(tc.path, node.Content[0], tc.addNode) + } else { + t.Fatal("unexpected node structure") + } + + out, err := yaml.Marshal(&node) + if err != nil { + t.Fatalf("failed to marshal result: %v", err) + } + + // Unmarshal both to compare semantically rather than as strings + var gotMap, expectedMap interface{} + if err := yaml.Unmarshal(out, &gotMap); err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + if err := yaml.Unmarshal([]byte(tc.expected), &expectedMap); err != nil { + t.Fatalf("failed to unmarshal expected: %v", err) + } + + // Compare the semantic content + gotYAML, _ := yaml.Marshal(gotMap) + expectedYAML, _ := yaml.Marshal(expectedMap) + if string(gotYAML) != string(expectedYAML) { + t.Errorf("expected:\n%s\ngot:\n%s", string(expectedYAML), string(gotYAML)) + } + }) + } +} + +func TestAddNodeEmptyPath(t *testing.T) { + initial := `key: value +` + + var node yaml.Node + err := yaml.Unmarshal([]byte(initial), &node) + if err != nil { + t.Fatalf("failed to unmarshal yaml: %v", err) + } + + addNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: "newvalue", + } + + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + AddNode([]string{}, node.Content[0], addNode) + } + + out, err := yaml.Marshal(&node) + if err != nil { + t.Fatalf("failed to marshal result: %v", err) + } + + if string(out) != initial { + t.Errorf("expected node to be unchanged with empty path, got:\n%s", string(out)) + } +} + +func TestAddNodePreservesExistingStructure(t *testing.T) { + initial := `database: + host: localhost + port: 5432 + credentials: + username: admin + password: secret +servers: + - name: server1 + ip: 192.168.1.1 + - name: server2 + ip: 192.168.1.2 +` + + var node yaml.Node + err := yaml.Unmarshal([]byte(initial), &node) + if err != nil { + t.Fatalf("failed to unmarshal yaml: %v", err) + } + + addNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: "newdb", + } + + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + AddNode([]string{"database", "name"}, node.Content[0], addNode) + } + + out, err := yaml.Marshal(&node) + if err != nil { + t.Fatalf("failed to marshal result: %v", err) + } + + var result map[string]interface{} + err = yaml.Unmarshal(out, &result) + if err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + + db, ok := result["database"].(map[string]interface{}) + if !ok { + t.Fatal("expected database to be a map") + } + + if db["host"] != "localhost" { + t.Errorf("expected host to be preserved as 'localhost', got %v", db["host"]) + } + if db["port"] != 5432 { + t.Errorf("expected port to be preserved as 5432, got %v", db["port"]) + } + if db["name"] != "newdb" { + t.Errorf("expected name to be 'newdb', got %v", db["name"]) + } + + servers, ok := result["servers"].([]interface{}) + if !ok { + t.Fatal("expected servers to be preserved as a sequence") + } + if len(servers) != 2 { + t.Errorf("expected 2 servers to be preserved, got %d", len(servers)) + } +} diff --git a/image-mapper/internal/yamlhelpers/walk.go b/image-mapper/internal/yamlhelpers/walk.go new file mode 100644 index 0000000..ab1fe42 --- /dev/null +++ b/image-mapper/internal/yamlhelpers/walk.go @@ -0,0 +1,37 @@ +package yamlhelpers + +import "gopkg.in/yaml.v3" + +// WalkNodeFn is called for each node by WalkNode +type WalkNodeFn func(path []string, node *yaml.Node) error + +// WalkNode walks recursively through a yaml.Node, calling fn for each node. +func WalkNode(node *yaml.Node, fn WalkNodeFn) error { + return walkNode([]string{}, node, fn) +} + +func walkNode(path []string, node *yaml.Node, fn WalkNodeFn) error { + if err := fn(path, node); err != nil { + return err + } + + switch node.Kind { + case yaml.MappingNode: + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + value := node.Content[i+1] + + if err := walkNode(append(path, key.Value), value, fn); err != nil { + return err + } + } + case yaml.SequenceNode: + for _, child := range node.Content { + if err := walkNode(path, child, fn); err != nil { + return err + } + } + } + + return nil +} diff --git a/image-mapper/internal/yamlhelpers/walk_test.go b/image-mapper/internal/yamlhelpers/walk_test.go new file mode 100644 index 0000000..a330923 --- /dev/null +++ b/image-mapper/internal/yamlhelpers/walk_test.go @@ -0,0 +1,297 @@ +package yamlhelpers + +import ( + "errors" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestWalkNode(t *testing.T) { + testCases := []struct { + name string + yaml string + expectedPaths []string + expectedKinds []yaml.Kind + }{ + { + name: "simple mapping", + yaml: ` +key1: value1 +key2: value2 +`, + expectedPaths: []string{ + "", + "key1", + "key2", + }, + expectedKinds: []yaml.Kind{ + yaml.MappingNode, + yaml.ScalarNode, + yaml.ScalarNode, + }, + }, + { + name: "nested mapping", + yaml: ` +parent: + child1: value1 + child2: value2 +`, + expectedPaths: []string{ + "", + "parent", + "parent.child1", + "parent.child2", + }, + expectedKinds: []yaml.Kind{ + yaml.MappingNode, + yaml.MappingNode, + yaml.ScalarNode, + yaml.ScalarNode, + }, + }, + { + name: "sequence", + yaml: ` +items: + - item1 + - item2 + - item3 +`, + expectedPaths: []string{ + "", + "items", + "items", + "items", + "items", + }, + expectedKinds: []yaml.Kind{ + yaml.MappingNode, + yaml.SequenceNode, + yaml.ScalarNode, + yaml.ScalarNode, + yaml.ScalarNode, + }, + }, + { + name: "mixed nested structure", + yaml: ` +database: + host: localhost + port: 5432 + credentials: + username: admin + password: secret +servers: + - name: server1 + ip: 192.168.1.1 + - name: server2 + ip: 192.168.1.2 +`, + expectedPaths: []string{ + "", + "database", + "database.host", + "database.port", + "database.credentials", + "database.credentials.username", + "database.credentials.password", + "servers", + "servers", + "servers.name", + "servers.ip", + "servers", + "servers.name", + "servers.ip", + }, + expectedKinds: []yaml.Kind{ + yaml.MappingNode, + yaml.MappingNode, + yaml.ScalarNode, + yaml.ScalarNode, + yaml.MappingNode, + yaml.ScalarNode, + yaml.ScalarNode, + yaml.SequenceNode, + yaml.MappingNode, + yaml.ScalarNode, + yaml.ScalarNode, + yaml.MappingNode, + yaml.ScalarNode, + yaml.ScalarNode, + }, + }, + { + name: "scalar only", + yaml: `simple value`, + expectedPaths: []string{ + "", + }, + expectedKinds: []yaml.Kind{ + yaml.ScalarNode, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var node yaml.Node + err := yaml.Unmarshal([]byte(tc.yaml), &node) + if err != nil { + t.Fatalf("failed to unmarshal yaml: %v", err) + } + + var paths []string + var kinds []yaml.Kind + + walkFn := func(path []string, n *yaml.Node) error { + pathStr := "" + if len(path) > 0 { + pathStr = path[0] + for i := 1; i < len(path); i++ { + pathStr += "." + path[i] + } + } + paths = append(paths, pathStr) + kinds = append(kinds, n.Kind) + return nil + } + + // Walk the content of the document node + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + err = WalkNode(node.Content[0], walkFn) + } else { + err = WalkNode(&node, walkFn) + } + if err != nil { + t.Fatalf("WalkNode returned error: %v", err) + } + + if len(paths) != len(tc.expectedPaths) { + t.Errorf("expected %d paths, got %d", len(tc.expectedPaths), len(paths)) + } + + for i := range paths { + if i >= len(tc.expectedPaths) { + break + } + if paths[i] != tc.expectedPaths[i] { + t.Errorf("path[%d]: expected %q, got %q", i, tc.expectedPaths[i], paths[i]) + } + } + + if len(kinds) != len(tc.expectedKinds) { + t.Errorf("expected %d kinds, got %d", len(tc.expectedKinds), len(kinds)) + } + + for i := range kinds { + if i >= len(tc.expectedKinds) { + break + } + if kinds[i] != tc.expectedKinds[i] { + t.Errorf("kind[%d]: expected %v, got %v", i, tc.expectedKinds[i], kinds[i]) + } + } + }) + } +} + +func TestWalkNodeError(t *testing.T) { + yamlContent := ` +key1: value1 +key2: value2 +key3: value3 +` + + var node yaml.Node + err := yaml.Unmarshal([]byte(yamlContent), &node) + if err != nil { + t.Fatalf("failed to unmarshal yaml: %v", err) + } + + expectedErr := errors.New("test error") + callCount := 0 + + walkFn := func(path []string, n *yaml.Node) error { + callCount++ + if callCount == 3 { + return expectedErr + } + return nil + } + + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + err = WalkNode(node.Content[0], walkFn) + } else { + err = WalkNode(&node, walkFn) + } + if err == nil { + t.Error("expected error to be returned") + } + + if err != expectedErr { + t.Errorf("expected error %v, got %v", expectedErr, err) + } + + if callCount != 3 { + t.Errorf("expected walkFn to be called 3 times, was called %d times", callCount) + } +} + +func TestWalkNodeModifyValues(t *testing.T) { + yamlContent := ` +key1: value1 +key2: value2 +nested: + key3: value3 +` + + var node yaml.Node + err := yaml.Unmarshal([]byte(yamlContent), &node) + if err != nil { + t.Fatalf("failed to unmarshal yaml: %v", err) + } + + walkFn := func(path []string, n *yaml.Node) error { + if n.Kind == yaml.ScalarNode && n.Value != "" && len(path) > 0 { + n.Value = "modified" + } + return nil + } + + if node.Kind == yaml.DocumentNode && len(node.Content) > 0 { + err = WalkNode(node.Content[0], walkFn) + } else { + err = WalkNode(&node, walkFn) + } + if err != nil { + t.Fatalf("WalkNode returned error: %v", err) + } + + out, err := yaml.Marshal(&node) + if err != nil { + t.Fatalf("failed to marshal modified yaml: %v", err) + } + + var result map[string]interface{} + err = yaml.Unmarshal(out, &result) + if err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + + if result["key1"] != "modified" { + t.Errorf("expected key1 to be 'modified', got %v", result["key1"]) + } + if result["key2"] != "modified" { + t.Errorf("expected key2 to be 'modified', got %v", result["key2"]) + } + + nested, ok := result["nested"].(map[string]interface{}) + if !ok { + t.Fatal("expected nested to be a map") + } + if nested["key3"] != "modified" { + t.Errorf("expected nested.key3 to be 'modified', got %v", nested["key3"]) + } +} From 86bfcb421cb9faa895b2b0c1a3ab6f2cef053d8d Mon Sep 17 00:00:00 2001 From: Rob Best Date: Mon, 5 Jan 2026 15:00:02 +0000 Subject: [PATCH 2/2] image-mapper: use cmd.Context --- image-mapper/cmd/map.go | 5 +---- image-mapper/cmd/map_helm.go | 9 ++------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/image-mapper/cmd/map.go b/image-mapper/cmd/map.go index 51851b7..c51bafd 100644 --- a/image-mapper/cmd/map.go +++ b/image-mapper/cmd/map.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "fmt" "os" @@ -27,8 +26,6 @@ func MapCommand() *cobra.Command { Short: "Map upstream image references to Chainguard images.", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.Background() - output, err := mapper.NewOutput(opts.OutputFormat) if err != nil { return fmt.Errorf("constructing output: %w", err) @@ -41,7 +38,7 @@ func MapCommand() *cobra.Command { if opts.IgnoreIamguarded { ignoreFns = append(ignoreFns, mapper.IgnoreIamguarded()) } - m, err := mapper.NewMapper(ctx, mapper.WithRepository(opts.Repo), mapper.WithIgnoreFns(ignoreFns...)) + m, err := mapper.NewMapper(cmd.Context(), mapper.WithRepository(opts.Repo), mapper.WithIgnoreFns(ignoreFns...)) if err != nil { return fmt.Errorf("creating mapper: %w", err) } diff --git a/image-mapper/cmd/map_helm.go b/image-mapper/cmd/map_helm.go index b8caaa9..46ced36 100644 --- a/image-mapper/cmd/map_helm.go +++ b/image-mapper/cmd/map_helm.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "fmt" "io" "os" @@ -38,14 +37,12 @@ func MapHelmChartCommand() *cobra.Command { `, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.Background() - chart := helm.ChartDescriptor{ Name: args[0], Repository: opts.ChartRepo, Version: opts.ChartVersion, } - output, err := helm.MapChart(ctx, chart, mapper.WithRepository(opts.Repo)) + output, err := helm.MapChart(cmd.Context(), chart, mapper.WithRepository(opts.Repo)) if err != nil { return fmt.Errorf("mapping values: %w", err) } @@ -85,8 +82,6 @@ func MapHelmValuesCommand() *cobra.Command { `, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.Background() - var ( input []byte err error @@ -104,7 +99,7 @@ func MapHelmValuesCommand() *cobra.Command { } } - output, err := helm.MapValues(ctx, input, mapper.WithRepository(opts.Repo)) + output, err := helm.MapValues(cmd.Context(), input, mapper.WithRepository(opts.Repo)) if err != nil { return fmt.Errorf("mapping values: %w", err) }