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..c51bafd --- /dev/null +++ b/image-mapper/cmd/map.go @@ -0,0 +1,71 @@ +package cmd + +import ( + "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 { + 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(cmd.Context(), 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..46ced36 --- /dev/null +++ b/image-mapper/cmd/map_helm.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "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 { + chart := helm.ChartDescriptor{ + Name: args[0], + Repository: opts.ChartRepo, + Version: opts.ChartVersion, + } + output, err := helm.MapChart(cmd.Context(), 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 { + 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(cmd.Context(), 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"]) + } +}