summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/dependabot.yml7
-rw-r--r--go.mod4
-rw-r--r--registry-scanner/config/example-config-ssh-config.yaml10
-rw-r--r--registry-scanner/config/example-config.yaml21
-rw-r--r--registry-scanner/config/example-grafana-dashboard.json685
-rw-r--r--registry-scanner/go.mod79
-rw-r--r--registry-scanner/go.sum609
-rw-r--r--registry-scanner/pkg/cache/memcache_test.go2
-rw-r--r--registry-scanner/pkg/common/constants.go65
-rw-r--r--registry-scanner/pkg/env/env.go2
-rw-r--r--registry-scanner/pkg/image/credentials.go261
-rw-r--r--registry-scanner/pkg/image/credentials_test.go410
-rw-r--r--registry-scanner/pkg/image/image.go275
-rw-r--r--registry-scanner/pkg/image/image_test.go226
-rw-r--r--registry-scanner/pkg/image/kustomize.go39
-rw-r--r--registry-scanner/pkg/image/kustomize_test.go26
-rw-r--r--registry-scanner/pkg/image/matchfunc.go27
-rw-r--r--registry-scanner/pkg/image/matchfunc_test.go27
-rw-r--r--registry-scanner/pkg/image/options.go296
-rw-r--r--registry-scanner/pkg/image/options_test.go493
-rw-r--r--registry-scanner/pkg/image/version.go220
-rw-r--r--registry-scanner/pkg/image/version_test.go196
-rw-r--r--registry-scanner/pkg/kube/kubernetes.go7
-rw-r--r--registry-scanner/pkg/registry/registry_test.go202
-rw-r--r--registry-scanner/test/fixture/fileutil.go14
-rw-r--r--registry-scanner/test/fixture/kubernetes.go63
-rw-r--r--registry-scanner/test/testdata/docker/invalid1-config.json7
-rw-r--r--registry-scanner/test/testdata/docker/valid-config-noproto.json7
-rw-r--r--registry-scanner/test/testdata/docker/valid-config.json7
-rw-r--r--registry-scanner/test/testdata/kubernetes/config19
-rw-r--r--registry-scanner/test/testdata/registry/config/two-defaults.yaml9
-rw-r--r--registry-scanner/test/testdata/resources/dummy-secret.json12
-rwxr-xr-xregistry-scanner/test/testdata/scripts/get-credentials-invalid.sh3
-rwxr-xr-xregistry-scanner/test/testdata/scripts/get-credentials-valid.sh3
34 files changed, 4186 insertions, 147 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 46a81a9..845dbd9 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -7,6 +7,13 @@ updates:
ignore:
- dependency-name: k8s.io/*
+ - package-ecosystem: "gomod"
+ directory: "/registry-scanner/"
+ schedule:
+ interval: "monthly"
+ ignore:
+ - dependency-name: k8s.io/*
+
- package-ecosystem: "github-actions"
directory: "/"
schedule:
diff --git a/go.mod b/go.mod
index 43b2a2d..6e12710 100644
--- a/go.mod
+++ b/go.mod
@@ -175,6 +175,7 @@ require (
)
replace (
+ github.com/argoproj-labs/argocd-image-updater/registry-scanner => ../registry-scanner
github.com/docker/distribution => github.com/docker/distribution v2.8.1+incompatible
github.com/golang/protobuf => github.com/golang/protobuf v1.5.4
@@ -209,7 +210,4 @@ replace (
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.31.0
k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.31.0
k8s.io/sample-controller => k8s.io/sample-controller v0.31.0
-
)
-
-replace github.com/argoproj-labs/argocd-image-updater/registry-scanner => ../registry-scanner
diff --git a/registry-scanner/config/example-config-ssh-config.yaml b/registry-scanner/config/example-config-ssh-config.yaml
new file mode 100644
index 0000000..3a2c960
--- /dev/null
+++ b/registry-scanner/config/example-config-ssh-config.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: registry-scanner-ssh-config
+ namespace: argocd
+data:
+ config: |
+ Host *
+ PubkeyAcceptedAlgorithms +ssh-rsa
+ HostkeyAlgorithms +ssh-rsa
diff --git a/registry-scanner/config/example-config.yaml b/registry-scanner/config/example-config.yaml
new file mode 100644
index 0000000..d0fd651
--- /dev/null
+++ b/registry-scanner/config/example-config.yaml
@@ -0,0 +1,21 @@
+# Example configuration for argocd-image-controller
+registries:
+- name: Docker Hub
+ api_url: https://registry-1.docker.io
+ prefix: docker.io
+ credentials: env:SOME_ENV_VAR
+ default: true
+ defaultns: library
+- name: Google Container Registry
+ api_url: https://gcr.io
+ prefix: gcr.io
+ credentials: pullsecret:foo/bar
+- name: RedHat Quay
+ api_url: https://quay.io
+ prefix: quay.io
+ credentials: secret:foo/bar#creds
+- name: GitHub Container Registry
+ api_url: https://ghcr.io
+ prefix: ghcr.io
+ credentials: ext:/some/script
+ credsexpire: 5h
diff --git a/registry-scanner/config/example-grafana-dashboard.json b/registry-scanner/config/example-grafana-dashboard.json
new file mode 100644
index 0000000..65246ca
--- /dev/null
+++ b/registry-scanner/config/example-grafana-dashboard.json
@@ -0,0 +1,685 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "description": "Example dashboard for Argo CD Image Updater metrics",
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "id": 25,
+ "links": [],
+ "panels": [
+ {
+ "collapsed": false,
+ "datasource": null,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 6,
+ "panels": [],
+ "title": "Configuration",
+ "type": "row"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "The total number of applications watched by Argo CD Image Updater",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 9,
+ "w": 12,
+ "x": 0,
+ "y": 1
+ },
+ "hiddenSeries": false,
+ "id": 2,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(argocd_image_updater_applications_watched_total)",
+ "instant": false,
+ "interval": "",
+ "legendFormat": "Applications",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Number of applications watched",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "Number of apps",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "Total number of images watched for updates by Argo CD Image Updater",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 9,
+ "w": 12,
+ "x": 12,
+ "y": 1
+ },
+ "hiddenSeries": false,
+ "id": 4,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(argocd_image_updater_images_watched_total)",
+ "interval": "",
+ "legendFormat": "Images",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Number of images watched",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "Number of images",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "collapsed": false,
+ "datasource": null,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 10
+ },
+ "id": 12,
+ "panels": [],
+ "title": "Image updates",
+ "type": "row"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "The total number and error of images updated at a given time",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 11
+ },
+ "hiddenSeries": false,
+ "id": 8,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(increase(argocd_image_updater_images_updated_total[1m]))",
+ "interval": "",
+ "legendFormat": "Updates",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(increase(argocd_image_updater_images_updated_error_total[1m]))",
+ "interval": "",
+ "legendFormat": "Errors",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Image updates (total)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "Amount",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 11
+ },
+ "hiddenSeries": false,
+ "id": 10,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (application) (increase(argocd_image_updater_images_updated_total[5m]))",
+ "interval": "",
+ "legendFormat": "{{application}}",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Image updates (per app)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "collapsed": false,
+ "datasource": null,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 19
+ },
+ "id": 18,
+ "panels": [],
+ "title": "Operational metrics",
+ "type": "row"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "Number of API requests to registries in a specific time frame",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 20
+ },
+ "hiddenSeries": false,
+ "id": 14,
+ "legend": {
+ "alignAsTable": true,
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (registry) (increase(argocd_image_updater_registry_requests_total[1m]))",
+ "interval": "",
+ "legendFormat": "{{registry}} | requests",
+ "refId": "A"
+ },
+ {
+ "expr": "sum by (registry) (increase(argocd_image_updater_registry_requests_failed_total[1m]))",
+ "interval": "",
+ "legendFormat": "{{registry}} | errors",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Registry API requests",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "The number of requests to the Argo CD API server over time",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 20
+ },
+ "hiddenSeries": false,
+ "id": 16,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(increase(argocd_image_updater_argocd_api_requests_total[1m]))",
+ "interval": "",
+ "legendFormat": "Requests",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(increase(argocd_image_updater_argocd_api_errors_total[1m]))",
+ "interval": "",
+ "legendFormat": "Errors",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Argo CD API requests",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ }
+ ],
+ "schemaVersion": 26,
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-6h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Argo CD Image Updater",
+ "uid": "M-U4ZLAMz",
+ "version": 9
+}
diff --git a/registry-scanner/go.mod b/registry-scanner/go.mod
index 00a1589..27d3f3e 100644
--- a/registry-scanner/go.mod
+++ b/registry-scanner/go.mod
@@ -1,22 +1,83 @@
module github.com/argoproj-labs/argocd-image-updater/registry-scanner
-go 1.22.3
+go 1.22.0
require (
+ github.com/Masterminds/semver/v3 v3.3.0
+ github.com/argoproj/pkg v0.13.7-0.20230627120311-a4dd357b057e
github.com/distribution/distribution/v3 v3.0.0-20230722181636-7b502560cad4
+ github.com/opencontainers/go-digest v1.0.0
+ github.com/opencontainers/image-spec v1.1.0
+ github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.9.3
- github.com/stretchr/testify v1.9.0
- k8s.io/api v0.31.2
- k8s.io/apimachinery v0.31.2
- k8s.io/client-go v0.31.2
- sigs.k8s.io/kustomize/api v0.12.1
- sigs.k8s.io/kustomize/kyaml v0.13.9
+ github.com/stretchr/testify v1.10.0
+ go.uber.org/ratelimit v0.3.1
+ golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
+ golang.org/x/sync v0.9.0
+ gopkg.in/yaml.v2 v2.4.0
+ k8s.io/api v0.31.0
+ k8s.io/apimachinery v0.31.0
+ k8s.io/client-go v1.5.2
)
require (
- github.com/Masterminds/semver/v3 v3.2.1
+ github.com/benbjohnson/clock v1.3.0 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/docker/go-metrics v0.0.1 // indirect
+ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
+ github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-openapi/jsonpointer v0.19.6 // indirect
+ github.com/go-openapi/jsonreference v0.20.2 // indirect
+ github.com/go-openapi/swag v0.22.4 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/gnostic-models v0.6.8 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/mux v1.8.0 // indirect
+ github.com/hashicorp/golang-lru v0.5.4 // indirect
+ github.com/imdario/mergo v0.3.16 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.16.5 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- golang.org/x/sys v0.21.0 // indirect
+ github.com/prometheus/client_golang v1.19.1 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/common v0.48.0 // indirect
+ github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/stretchr/objx v0.5.2 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ golang.org/x/net v0.28.0 // indirect
+ golang.org/x/oauth2 v0.22.0 // indirect
+ golang.org/x/sys v0.24.0 // indirect
+ golang.org/x/term v0.23.0 // indirect
+ golang.org/x/text v0.17.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
+ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
+ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
+ sigs.k8s.io/yaml v1.4.0 // indirect
+)
+
+replace (
+ k8s.io/api => k8s.io/api v0.31.0
+ k8s.io/apimachinery => k8s.io/apimachinery v0.31.2
+ k8s.io/client-go => k8s.io/client-go v0.31.0
)
diff --git a/registry-scanner/go.sum b/registry-scanner/go.sum
index 0eff022..4dfb551 100644
--- a/registry-scanner/go.sum
+++ b/registry-scanner/go.sum
@@ -1,68 +1,619 @@
-github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
-github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
-github.com/argoproj-labs/argocd-image-updater v0.14.0 h1:DICeW/eVROJpdjiuQxMoEGYnyzMMjjgYDVUrkACH+vM=
-github.com/argoproj-labs/argocd-image-updater v0.14.0/go.mod h1:PSVBweUoS6ogVFAikCTTNbXoZ5+pJT9ksG45rwsQqi0=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
+github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/argoproj/pkg v0.13.7-0.20230627120311-a4dd357b057e h1:kuLQvJqwwRMQTheT4MFyKVM8Txncu21CHT4yBWUl1Mk=
+github.com/argoproj/pkg v0.13.7-0.20230627120311-a4dd357b057e/go.mod h1:xBN5PLx2MoK63dmPfMo/PGBvd77K1Y0m/rzZOe4cs1s=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/aws/aws-sdk-go v1.44.290/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+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/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
+github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
+github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/distribution/distribution/v3 v3.0.0-20230722181636-7b502560cad4 h1:DstcWc/NnRAc1hkOJm67dl4dgeQm/Gvl965lfZyOgRI=
github.com/distribution/distribution/v3 v3.0.0-20230722181636-7b502560cad4/go.mod h1:+fqBJ4vPYo4Uu1ZE4d+bUtTLRXfdSL3NvCZIZ9GHv58=
-github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
-github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+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/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
+github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+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.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
+github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+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/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+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.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=
+github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
+github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+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.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
+github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+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/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.58/go.mod h1:NUDy4A4oXPq1l2yK6LTSvCEzAMeIcoz9lcj5dbzSrRE=
+github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
+github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
+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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
+github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
+github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
+github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
+github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw=
+github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo=
+github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc=
+github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk=
+github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo=
+github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
+github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
+github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
+github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
+github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
+github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
+github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
+github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
+github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
+github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
+github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
+github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
+github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
+github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
+github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
+github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw=
+github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
+github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
+github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
+github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ=
+github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
+github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
+github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
+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.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
+github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
+go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
+golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+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.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
+golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
+golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+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-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
+golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
+golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
+golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
+golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
+golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
+golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+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=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.0-20210107192922-496545a6307b/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=
-k8s.io/api v0.26.11 h1:hLhTZRdYc3vBBOY4wbEyTLWgMyieOAk2Ws9NG57QqO4=
-k8s.io/api v0.26.11/go.mod h1:bSr/A0TKRt5W2OMDdexkM/ER1NxOxiQqNNFXW2nMZrM=
-k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
-k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
-k8s.io/apimachinery v0.26.11 h1:w//840HHdwSRKqD15j9YX9HLlU6RPlfrvW0xEhLk2+0=
-k8s.io/apimachinery v0.26.11/go.mod h1:2/HZp0l6coXtS26du1Bk36fCuAEr/lVs9Q9NbpBtd1Y=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
+k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
+k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
-k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
-k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
-k8s.io/client-go v1.5.2 h1:JOxmv4FxrCIOS54kAABbN8/hA9jqGpns+Zc6soNgd8U=
-k8s.io/client-go v1.5.2/go.mod h1:OmM68YRko3DQ0sjlnWxzjQF9lcSLHJXuGMTo23rc7wI=
-k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
-k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk=
-k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
+k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
+k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
+k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/klog/v2 v2.5.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
+k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+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-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
+k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
+k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
+k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
-sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s=
-sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/registry-scanner/pkg/cache/memcache_test.go b/registry-scanner/pkg/cache/memcache_test.go
index 8fcb47b..ff8bc5f 100644
--- a/registry-scanner/pkg/cache/memcache_test.go
+++ b/registry-scanner/pkg/cache/memcache_test.go
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/argoproj-labs/argocd-image-updater/pkg/tag"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag"
)
func Test_MemCache(t *testing.T) {
diff --git a/registry-scanner/pkg/common/constants.go b/registry-scanner/pkg/common/constants.go
new file mode 100644
index 0000000..eeb9ccb
--- /dev/null
+++ b/registry-scanner/pkg/common/constants.go
@@ -0,0 +1,65 @@
+package common
+
+// This file contains a list of constants required by other packages
+
+const ImageUpdaterAnnotationPrefix = "argocd-image-updater.argoproj.io"
+
+// The annotation on the application resources to indicate the list of images
+// allowed for updates.
+const ImageUpdaterAnnotation = ImageUpdaterAnnotationPrefix + "/image-list"
+
+// Defaults for Helm parameter names
+const (
+ DefaultHelmImageName = "image.name"
+ DefaultHelmImageTag = "image.tag"
+)
+
+// Helm related annotations
+const (
+ HelmParamImageNameAnnotation = ImageUpdaterAnnotationPrefix + "/%s.helm.image-name"
+ HelmParamImageTagAnnotation = ImageUpdaterAnnotationPrefix + "/%s.helm.image-tag"
+ HelmParamImageSpecAnnotation = ImageUpdaterAnnotationPrefix + "/%s.helm.image-spec"
+)
+
+// Kustomize related annotations
+const (
+ KustomizeApplicationNameAnnotation = ImageUpdaterAnnotationPrefix + "/%s.kustomize.image-name"
+)
+
+// Image specific configuration annotations
+const (
+ OldMatchOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.tag-match" // Deprecated and will be removed
+ AllowTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.allow-tags"
+ IgnoreTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.ignore-tags"
+ ForceUpdateOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.force-update"
+ UpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/%s.update-strategy"
+ PullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/%s.pull-secret"
+ PlatformsAnnotation = ImageUpdaterAnnotationPrefix + "/%s.platforms"
+)
+
+// Application-wide update strategy related annotations
+const (
+ ApplicationWideAllowTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/allow-tags"
+ ApplicationWideIgnoreTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/ignore-tags"
+ ApplicationWideForceUpdateOptionAnnotation = ImageUpdaterAnnotationPrefix + "/force-update"
+ ApplicationWideUpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/update-strategy"
+ ApplicationWidePullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/pull-secret"
+)
+
+// Application update configuration related annotations
+const (
+ WriteBackMethodAnnotation = ImageUpdaterAnnotationPrefix + "/write-back-method"
+ GitBranchAnnotation = ImageUpdaterAnnotationPrefix + "/git-branch"
+ GitRepositoryAnnotation = ImageUpdaterAnnotationPrefix + "/git-repository"
+ WriteBackTargetAnnotation = ImageUpdaterAnnotationPrefix + "/write-back-target"
+ KustomizationPrefix = "kustomization"
+ HelmPrefix = "helmvalues"
+)
+
+// The default Git commit message's template
+const DefaultGitCommitMessage = `build: automatic update of {{ .AppName }}
+
+{{ range .AppChanges -}}
+updates image {{ .Image }} tag '{{ .OldTag }}' to '{{ .NewTag }}'
+{{ end -}}
+`
diff --git a/registry-scanner/pkg/env/env.go b/registry-scanner/pkg/env/env.go
index e780067..f000aa2 100644
--- a/registry-scanner/pkg/env/env.go
+++ b/registry-scanner/pkg/env/env.go
@@ -6,7 +6,7 @@ import (
"strconv"
"strings"
- "github.com/argoproj-labs/argocd-image-updater/pkg/log"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
)
// Package env provides some utility functions to interact with the environment
diff --git a/registry-scanner/pkg/image/credentials.go b/registry-scanner/pkg/image/credentials.go
new file mode 100644
index 0000000..a19d01a
--- /dev/null
+++ b/registry-scanner/pkg/image/credentials.go
@@ -0,0 +1,261 @@
+package image
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ argoexec "github.com/argoproj/pkg/exec"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
+)
+
+type CredentialSourceType int
+
+const (
+ CredentialSourceUnknown CredentialSourceType = 0
+ CredentialSourcePullSecret CredentialSourceType = 1
+ CredentialSourceSecret CredentialSourceType = 2
+ CredentialSourceEnv CredentialSourceType = 3
+ CredentialSourceExt CredentialSourceType = 4
+)
+
+type CredentialSource struct {
+ Type CredentialSourceType
+ Registry string
+ SecretNamespace string
+ SecretName string
+ SecretField string
+ EnvName string
+ ScriptPath string
+}
+
+type Credential struct {
+ Username string
+ Password string
+}
+
+const pullSecretField = ".dockerconfigjson"
+
+// gcr.io=secret:foo/bar#baz
+// gcr.io=pullsecret:foo/bar
+// gcr.io=env:FOOBAR
+
+func ParseCredentialSource(credentialSource string, requirePrefix bool) (*CredentialSource, error) {
+ src := CredentialSource{}
+ var secretDef string
+ tokens := strings.SplitN(credentialSource, "=", 2)
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ if requirePrefix {
+ return nil, fmt.Errorf("invalid credential spec: %s", credentialSource)
+ }
+ secretDef = credentialSource
+ } else {
+ src.Registry = tokens[0]
+ secretDef = tokens[1]
+ }
+
+ tokens = strings.Split(secretDef, ":")
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return nil, fmt.Errorf("invalid credential spec: %s", credentialSource)
+ }
+
+ var err error
+ switch strings.ToLower(tokens[0]) {
+ case "secret":
+ err = src.parseSecretDefinition(tokens[1])
+ src.Type = CredentialSourceSecret
+ case "pullsecret":
+ err = src.parsePullSecretDefinition(tokens[1])
+ src.Type = CredentialSourcePullSecret
+ case "env":
+ err = src.parseEnvDefinition(tokens[1])
+ src.Type = CredentialSourceEnv
+ case "ext":
+ err = src.parseExtDefinition(tokens[1])
+ src.Type = CredentialSourceExt
+ default:
+ err = fmt.Errorf("unknown credential source: %s", tokens[0])
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &src, nil
+}
+
+// FetchCredentials fetches the credentials for a given registry according to
+// the credential source.
+func (src *CredentialSource) FetchCredentials(registryURL string, kubeclient *kube.KubernetesClient) (*Credential, error) {
+ var creds Credential
+ log.Tracef("Fetching credentials for registry %s", registryURL)
+ switch src.Type {
+ case CredentialSourceEnv:
+ credEnv := os.Getenv(src.EnvName)
+ if credEnv == "" {
+ return nil, fmt.Errorf("could not fetch credentials: env '%s' is not set", src.EnvName)
+ }
+ tokens := strings.SplitN(credEnv, ":", 2)
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return nil, fmt.Errorf("could not fetch credentials: value of %s is malformed", src.EnvName)
+ }
+ creds.Username = tokens[0]
+ creds.Password = tokens[1]
+ return &creds, nil
+ case CredentialSourceSecret:
+ if kubeclient == nil {
+ return nil, fmt.Errorf("could not fetch credentials: no Kubernetes client given")
+ }
+ data, err := kubeclient.GetSecretField(src.SecretNamespace, src.SecretName, src.SecretField)
+ if err != nil {
+ return nil, fmt.Errorf("could not fetch secret '%s' from namespace '%s' (field: '%s'): %v", src.SecretName, src.SecretNamespace, src.SecretField, err)
+ }
+ tokens := strings.SplitN(data, ":", 2)
+ if len(tokens) != 2 {
+ return nil, fmt.Errorf("invalid credentials in secret '%s' from namespace '%s' (field '%s')", src.SecretName, src.SecretNamespace, src.SecretField)
+ }
+ creds.Username = tokens[0]
+ creds.Password = tokens[1]
+ return &creds, nil
+ case CredentialSourcePullSecret:
+ if kubeclient == nil {
+ return nil, fmt.Errorf("could not fetch credentials: no Kubernetes client given")
+ }
+ src.SecretField = pullSecretField
+ data, err := kubeclient.GetSecretField(src.SecretNamespace, src.SecretName, src.SecretField)
+ if err != nil {
+ return nil, fmt.Errorf("could not fetch secret '%s' from namespace '%s' (field: '%s'): %v", src.SecretName, src.SecretNamespace, src.SecretField, err)
+ }
+ creds.Username, creds.Password, err = parseDockerConfigJson(registryURL, data)
+ if err != nil {
+ return nil, err
+ }
+ return &creds, nil
+ case CredentialSourceExt:
+ if !strings.HasPrefix(src.ScriptPath, "/") {
+ return nil, fmt.Errorf("path to script must be absolute, but is '%s'", src.ScriptPath)
+ }
+ _, err := os.Stat(src.ScriptPath)
+ if err != nil {
+ return nil, fmt.Errorf("could not stat %s: %v", src.ScriptPath, err)
+ }
+ cmd := exec.Command(src.ScriptPath)
+ out, err := argoexec.RunCommandExt(cmd, argoexec.CmdOpts{Timeout: 10 * time.Second})
+ if err != nil {
+ return nil, fmt.Errorf("error executing %s: %v", src.ScriptPath, err)
+ }
+ tokens := strings.SplitN(out, ":", 2)
+ if len(tokens) != 2 {
+ return nil, fmt.Errorf("invalid script output, must be single line with syntax <username>:<password>")
+ }
+ creds.Username = tokens[0]
+ creds.Password = tokens[1]
+ return &creds, nil
+
+ default:
+ return nil, fmt.Errorf("unknown credential type")
+ }
+}
+
+// Parse a secret definition in form of 'namespace/name#field'
+func (src *CredentialSource) parseSecretDefinition(definition string) error {
+ tokens := strings.Split(definition, "#")
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return fmt.Errorf("invalid secret definition: %s", definition)
+ }
+ src.SecretField = tokens[1]
+ tokens = strings.Split(tokens[0], "/")
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return fmt.Errorf("invalid secret definition: %s", definition)
+ }
+ src.SecretNamespace = tokens[0]
+ src.SecretName = tokens[1]
+
+ return nil
+}
+
+// Parse an image pull secret definition in form of 'namespace/name'
+func (src *CredentialSource) parsePullSecretDefinition(definition string) error {
+ tokens := strings.Split(definition, "/")
+ if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" {
+ return fmt.Errorf("invalid secret definition: %s", definition)
+ }
+
+ src.SecretNamespace = tokens[0]
+ src.SecretName = tokens[1]
+ src.SecretField = pullSecretField
+
+ return nil
+}
+
+// Parse an environment definition
+// nolint:unparam
+func (src *CredentialSource) parseEnvDefinition(definition string) error {
+ src.EnvName = definition
+ return nil
+}
+
+// Parse an external script definition
+// nolint:unparam
+func (src *CredentialSource) parseExtDefinition(definition string) error {
+ src.ScriptPath = definition
+ return nil
+}
+
+// This unmarshals & parses Docker's config.json file, returning username and
+// password for given registry URL
+func parseDockerConfigJson(registryURL string, jsonSource string) (string, string, error) {
+ var dockerConf map[string]interface{}
+ err := json.Unmarshal([]byte(jsonSource), &dockerConf)
+ if err != nil {
+ return "", "", err
+ }
+ auths, ok := dockerConf["auths"].(map[string]interface{})
+ if !ok {
+ return "", "", fmt.Errorf("no credentials in image pull secret")
+ }
+
+ var regPrefix string
+ if strings.HasPrefix(registryURL, "http://") {
+ regPrefix = strings.TrimPrefix(registryURL, "http://")
+ } else if strings.HasPrefix(registryURL, "https://") {
+ regPrefix = strings.TrimPrefix(registryURL, "https://")
+ } else {
+ regPrefix = registryURL
+ }
+
+ regPrefix = strings.TrimSuffix(regPrefix, "/")
+
+ for registry, authConf := range auths {
+ if !strings.HasPrefix(registry, registryURL) && !strings.HasPrefix(registry, regPrefix) {
+ log.Tracef("found registry %s in image pull secret, but we want %s (%s) - skipping", registry, registryURL, regPrefix)
+ continue
+ }
+ authEntry, ok := authConf.(map[string]interface{})
+ if !ok {
+ return "", "", fmt.Errorf("invalid auth entry for registry entry %s ('auths' entry should be map)", registry)
+ }
+ authString, ok := authEntry["auth"].(string)
+ if !ok {
+ return "", "", fmt.Errorf("invalid auth token for registry entry %s ('auth' should be string')", registry)
+ }
+ authToken, err := base64.StdEncoding.DecodeString(authString)
+ if err != nil {
+ return "", "", fmt.Errorf("could not base64-decode auth data for registry entry %s: %v", registry, err)
+ }
+ tokens := strings.SplitN(string(authToken), ":", 2)
+ if len(tokens) != 2 {
+ return "", "", fmt.Errorf("invalid data after base64 decoding auth entry for registry entry %s", registry)
+ }
+
+ return tokens[0], tokens[1], nil
+ }
+
+ return "", "", fmt.Errorf("no valid auth entry for registry %s found in image pull secret", registryURL)
+}
diff --git a/registry-scanner/pkg/image/credentials_test.go b/registry-scanner/pkg/image/credentials_test.go
new file mode 100644
index 0000000..6d53bf4
--- /dev/null
+++ b/registry-scanner/pkg/image/credentials_test.go
@@ -0,0 +1,410 @@
+package image
+
+import (
+ "fmt"
+ "os"
+ "path"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/test/fake"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/test/fixture"
+)
+
+func Test_ParseCredentialAnnotation(t *testing.T) {
+ t.Run("Parse valid credentials definition of type secret", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:mynamespace/mysecret#anyfield", true)
+ assert.NoError(t, err)
+ assert.Equal(t, "gcr.io", src.Registry)
+ assert.Equal(t, "mynamespace", src.SecretNamespace)
+ assert.Equal(t, "mysecret", src.SecretName)
+ assert.Equal(t, "anyfield", src.SecretField)
+ })
+
+ t.Run("Parse valid credentials definition of type pullsecret", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace/mysecret", true)
+ assert.NoError(t, err)
+ assert.Equal(t, "gcr.io", src.Registry)
+ assert.Equal(t, "mynamespace", src.SecretNamespace)
+ assert.Equal(t, "mysecret", src.SecretName)
+ assert.Equal(t, ".dockerconfigjson", src.SecretField)
+ })
+
+ t.Run("Parse invalid secret definition - missing registry", func(t *testing.T) {
+ src, err := ParseCredentialSource("secret:mynamespace/mysecret#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid secret definition - empty registry", func(t *testing.T) {
+ src, err := ParseCredentialSource("=secret:mynamespace/mysecret#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid secret definition - unknown credential type", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secrets:mynamespace/mysecret#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid secret definition - missing field", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:mynamespace/mysecret#", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid secret definition - missing namespace", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:/mysecret#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid credential definition - missing name", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:mynamespace/#anyfield", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid credential definition - missing most", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=secret:", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid pullsecret definition - missing namespace", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=pullsecret:/mysecret", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse invalid credential definition - missing name", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace", true)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+ t.Run("Parse valid credentials definition from environment", func(t *testing.T) {
+ src, err := ParseCredentialSource("env:DUMMY_SECRET", false)
+ require.NoError(t, err)
+ require.NotNil(t, src)
+ assert.Equal(t, "DUMMY_SECRET", src.EnvName)
+ })
+
+ t.Run("Parse valid credentials definition from environment", func(t *testing.T) {
+ src, err := ParseCredentialSource("env:DUMMY_SECRET", false)
+ require.NoError(t, err)
+ require.NotNil(t, src)
+ assert.Equal(t, "DUMMY_SECRET", src.EnvName)
+ })
+
+ t.Run("Parse external script credentials", func(t *testing.T) {
+ src, err := ParseCredentialSource("ext:/tmp/a.sh", false)
+ require.NoError(t, err)
+ assert.Equal(t, CredentialSourceExt, src.Type)
+ assert.Equal(t, "/tmp/a.sh", src.ScriptPath)
+ })
+}
+
+func Test_ParseCredentialReference(t *testing.T) {
+ t.Run("Parse valid credentials definition of type secret", func(t *testing.T) {
+ src, err := ParseCredentialSource("secret:mynamespace/mysecret#anyfield", false)
+ assert.NoError(t, err)
+ assert.Equal(t, "", src.Registry)
+ assert.Equal(t, "mynamespace", src.SecretNamespace)
+ assert.Equal(t, "mysecret", src.SecretName)
+ assert.Equal(t, "anyfield", src.SecretField)
+ })
+
+ t.Run("Parse valid credentials definition of type pullsecret", func(t *testing.T) {
+ src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace/mysecret", false)
+ assert.NoError(t, err)
+ assert.Equal(t, "gcr.io", src.Registry)
+ assert.Equal(t, "mynamespace", src.SecretNamespace)
+ assert.Equal(t, "mysecret", src.SecretName)
+ assert.Equal(t, ".dockerconfigjson", src.SecretField)
+ })
+
+ t.Run("Parse invalid secret definition - empty registry", func(t *testing.T) {
+ src, err := ParseCredentialSource("=secret:mynamespace/mysecret#anyfield", false)
+ assert.Error(t, err)
+ assert.Nil(t, src)
+ })
+
+}
+
+func Test_FetchCredentialsFromSecret(t *testing.T) {
+ t.Run("Fetch credentials from secret", func(t *testing.T) {
+ secretData := make(map[string][]byte)
+ secretData["username_password"] = []byte(fmt.Sprintf("%s:%s", "foo", "bar"))
+ secret := fixture.NewSecret("test", "test", secretData)
+ clientset := fake.NewFakeClientsetWithResources(secret)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceSecret,
+ SecretNamespace: "test",
+ SecretName: "test",
+ SecretField: "username_password",
+ }
+ creds, err := credSrc.FetchCredentials("NA", &kube.KubernetesClient{Clientset: clientset})
+ require.NoError(t, err)
+ require.NotNil(t, creds)
+ assert.Equal(t, "foo", creds.Username)
+ assert.Equal(t, "bar", creds.Password)
+
+ credSrc.SecretNamespace = "test1" // test with a wrong SecretNamespace
+ creds, err = credSrc.FetchCredentials("NA", &kube.KubernetesClient{Clientset: clientset})
+ require.Error(t, err)
+ require.Nil(t, creds)
+ })
+
+ t.Run("Fetch credentials from secret with invalid config", func(t *testing.T) {
+ secretData := make(map[string][]byte)
+ secretData["username_password"] = []byte(fmt.Sprintf("%s:%s", "foo", "bar"))
+ secret := fixture.NewSecret("test", "test", secretData)
+ clientset := fake.NewFakeClientsetWithResources(secret)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceSecret,
+ SecretNamespace: "test",
+ SecretName: "test",
+ SecretField: "username_password",
+ }
+ creds, err := credSrc.FetchCredentials("NA", nil)
+ require.Error(t, err) // should fail with "could not fetch credentials: no Kubernetes client given"
+ require.Nil(t, creds)
+
+ credSrc.SecretField = "BAD" // test with a wrong SecretField
+ creds, err = credSrc.FetchCredentials("NA", &kube.KubernetesClient{Clientset: clientset})
+ require.Error(t, err)
+ require.Nil(t, creds)
+
+ })
+}
+
+func Test_FetchCredentialsFromPullSecret(t *testing.T) {
+ t.Run("Fetch credentials from pull secret", func(t *testing.T) {
+ dockerJson := fixture.MustReadFile("../../test/testdata/docker/valid-config.json")
+ secretData := make(map[string][]byte)
+ secretData[pullSecretField] = []byte(dockerJson)
+ pullSecret := fixture.NewSecret("test", "test", secretData)
+ clientset := fake.NewFakeClientsetWithResources(pullSecret)
+ credSrc := &CredentialSource{
+ Type: CredentialSourcePullSecret,
+ Registry: "https://registry-1.docker.io/v2",
+ SecretNamespace: "test",
+ SecretName: "test",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", &kube.KubernetesClient{Clientset: clientset})
+ require.NoError(t, err)
+ require.NotNil(t, creds)
+ assert.Equal(t, "foo", creds.Username)
+ assert.Equal(t, "bar", creds.Password)
+
+ credSrc.SecretNamespace = "test1" // test with a wrong SecretNamespace
+ creds, err = credSrc.FetchCredentials("https://registry-1.docker.io", &kube.KubernetesClient{Clientset: clientset})
+ require.Error(t, err)
+ require.Nil(t, creds)
+ })
+
+ t.Run("Fetch credentials from pull secret with invalid config", func(t *testing.T) {
+ dockerJson := fixture.MustReadFile("../../test/testdata/docker/valid-config.json")
+ dockerJson = strings.ReplaceAll(dockerJson, "auths", "BAD-KEY")
+ secretData := make(map[string][]byte)
+ secretData[pullSecretField] = []byte(dockerJson)
+ pullSecret := fixture.NewSecret("test", "test", secretData)
+ clientset := fake.NewFakeClientsetWithResources(pullSecret)
+ credSrc := &CredentialSource{
+ Type: CredentialSourcePullSecret,
+ Registry: "https://registry-1.docker.io/v2",
+ SecretNamespace: "test",
+ SecretName: "test",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", &kube.KubernetesClient{Clientset: clientset})
+ require.Error(t, err) // should fail with "no credentials in image pull secret"
+ require.Nil(t, creds)
+
+ creds, err = credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Error(t, err) // should fail with "could not fetch credentials: no Kubernetes client given"
+ require.Nil(t, creds)
+ })
+
+ t.Run("Fetch credentials from pull secret with protocol stripped", func(t *testing.T) {
+ dockerJson := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json")
+ secretData := make(map[string][]byte)
+ secretData[pullSecretField] = []byte(dockerJson)
+ pullSecret := fixture.NewSecret("test", "test", secretData)
+ clientset := fake.NewFakeClientsetWithResources(pullSecret)
+ credSrc := &CredentialSource{
+ Type: CredentialSourcePullSecret,
+ Registry: "https://registry-1.docker.io/v2",
+ SecretNamespace: "test",
+ SecretName: "test",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", &kube.KubernetesClient{Clientset: clientset})
+ require.NoError(t, err)
+ require.NotNil(t, creds)
+ assert.Equal(t, "foo", creds.Username)
+ assert.Equal(t, "bar", creds.Password)
+ })
+}
+
+func Test_FetchCredentialsFromEnv(t *testing.T) {
+ t.Run("Fetch credentials from environment", func(t *testing.T) {
+ err := os.Setenv("MY_SECRET_ENV", "foo:bar")
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceEnv,
+ Registry: "https://registry-1.docker.io/v2",
+ EnvName: "MY_SECRET_ENV",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.NoError(t, err)
+ require.NotNil(t, creds)
+ assert.Equal(t, "foo", creds.Username)
+ assert.Equal(t, "bar", creds.Password)
+ })
+
+ t.Run("Fetch credentials from environment with missing env var", func(t *testing.T) {
+ err := os.Setenv("MY_SECRET_ENV", "")
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceEnv,
+ Registry: "https://registry-1.docker.io/v2",
+ EnvName: "MY_SECRET_ENV",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Error(t, err)
+ require.Nil(t, creds)
+ })
+
+ t.Run("Fetch credentials from environment with invalid value in env var", func(t *testing.T) {
+ for _, value := range []string{"babayaga", "foo:", "bar:", ":"} {
+ err := os.Setenv("MY_SECRET_ENV", value)
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceEnv,
+ Registry: "https://registry-1.docker.io/v2",
+ EnvName: "MY_SECRET_ENV",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Error(t, err)
+ require.Nil(t, creds)
+ }
+ })
+}
+
+func Test_FetchCredentialsFromExt(t *testing.T) {
+ t.Run("Fetch credentials from external script - valid output", func(t *testing.T) {
+ pwd, err := os.Getwd()
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceExt,
+ Registry: "https://registry-1.docker.io/v2",
+ ScriptPath: path.Join(pwd, "..", "..", "test", "testdata", "scripts", "get-credentials-valid.sh"),
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.NoError(t, err)
+ require.NotNil(t, creds)
+ assert.Equal(t, "username", creds.Username)
+ assert.Equal(t, "password", creds.Password)
+ })
+ t.Run("Fetch credentials from external script - invalid script output", func(t *testing.T) {
+ pwd, err := os.Getwd()
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceExt,
+ Registry: "https://registry-1.docker.io/v2",
+ ScriptPath: path.Join(pwd, "..", "..", "test", "testdata", "scripts", "get-credentials-invalid.sh"),
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Errorf(t, err, "invalid script output")
+ require.Nil(t, creds)
+ })
+ t.Run("Fetch credentials from external script - script does not exist", func(t *testing.T) {
+ pwd, err := os.Getwd()
+ require.NoError(t, err)
+ credSrc := &CredentialSource{
+ Type: CredentialSourceExt,
+ Registry: "https://registry-1.docker.io/v2",
+ ScriptPath: path.Join(pwd, "..", "..", "test", "testdata", "scripts", "get-credentials-notexist.sh"),
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Errorf(t, err, "no such file or directory")
+ require.Nil(t, creds)
+ })
+ t.Run("Fetch credentials from external script - relative path", func(t *testing.T) {
+ credSrc := &CredentialSource{
+ Type: CredentialSourceExt,
+ Registry: "https://registry-1.docker.io/v2",
+ ScriptPath: "get-credentials-notexist.sh",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Errorf(t, err, "path to script must be absolute")
+ require.Nil(t, creds)
+ })
+}
+
+func Test_FetchCredentialsFromUnknown(t *testing.T) {
+ t.Run("Fetch credentials from unknown type", func(t *testing.T) {
+ credSrc := &CredentialSource{
+ Type: CredentialSourceType(-1),
+ Registry: "https://registry-1.docker.io/v2",
+ }
+ creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil)
+ require.Error(t, err) // should fail with "unknown credential type"
+ require.Nil(t, creds)
+ })
+}
+
+func Test_ParseDockerConfig(t *testing.T) {
+ t.Run("Parse valid Docker configuration with matching registry", func(t *testing.T) {
+ config := fixture.MustReadFile("../../test/testdata/docker/valid-config.json")
+ username, password, err := parseDockerConfigJson("https://registry-1.docker.io", config)
+ require.NoError(t, err)
+ assert.Equal(t, "foo", username)
+ assert.Equal(t, "bar", password)
+ })
+
+ t.Run("Parse valid Docker configuration with matching registry as prefix", func(t *testing.T) {
+ config := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json")
+ username, password, err := parseDockerConfigJson("https://registry-1.docker.io", config)
+ require.NoError(t, err)
+ assert.Equal(t, "foo", username)
+ assert.Equal(t, "bar", password)
+ })
+
+ t.Run("Parse valid Docker configuration with matching http registry as prefix", func(t *testing.T) {
+ config := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json")
+ username, password, err := parseDockerConfigJson("http://registry-1.docker.io", config)
+ require.NoError(t, err)
+ assert.Equal(t, "foo", username)
+ assert.Equal(t, "bar", password)
+ })
+
+ t.Run("Parse valid Docker configuration with matching no-protocol registry as prefix", func(t *testing.T) {
+ config := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json")
+ username, password, err := parseDockerConfigJson("registry-1.docker.io", config)
+ require.NoError(t, err)
+ assert.Equal(t, "foo", username)
+ assert.Equal(t, "bar", password)
+ })
+
+ t.Run("Parse valid Docker configuration with matching registry as prefix with / in the end", func(t *testing.T) {
+ config := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json")
+ username, password, err := parseDockerConfigJson("https://registry-1.docker.io/", config)
+ require.NoError(t, err)
+ assert.Equal(t, "foo", username)
+ assert.Equal(t, "bar", password)
+ })
+
+ t.Run("Parse valid Docker configuration without matching registry", func(t *testing.T) {
+ config := fixture.MustReadFile("../../test/testdata/docker/valid-config.json")
+ username, password, err := parseDockerConfigJson("https://gcr.io", config)
+ assert.Error(t, err)
+ assert.Empty(t, username)
+ assert.Empty(t, password)
+ })
+}
diff --git a/registry-scanner/pkg/image/image.go b/registry-scanner/pkg/image/image.go
new file mode 100644
index 0000000..01261be
--- /dev/null
+++ b/registry-scanner/pkg/image/image.go
@@ -0,0 +1,275 @@
+package image
+
+import (
+ "strings"
+ "time"
+
+ "github.com/distribution/distribution/v3/reference"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag"
+)
+
+type ContainerImage struct {
+ RegistryURL string
+ ImageName string
+ ImageTag *tag.ImageTag
+ ImageAlias string
+ HelmParamImageName string
+ HelmParamImageVersion string
+ KustomizeImage *ContainerImage
+ original string
+}
+
+type ContainerImageList []*ContainerImage
+
+// NewFromIdentifier parses an image identifier and returns a populated ContainerImage
+func NewFromIdentifier(identifier string) *ContainerImage {
+ imgRef := identifier
+ alias := ""
+ if strings.Contains(identifier, "=") {
+ n := strings.SplitN(identifier, "=", 2)
+ imgRef = n[1]
+ alias = n[0]
+ }
+ if parsed, err := reference.ParseNormalizedNamed(imgRef); err == nil {
+ img := ContainerImage{}
+ img.RegistryURL = reference.Domain(parsed)
+ // remove default registry for backwards-compatibility
+ if img.RegistryURL == "docker.io" && !strings.HasPrefix(imgRef, "docker.io") {
+ img.RegistryURL = ""
+ }
+ img.ImageAlias = alias
+ img.ImageName = reference.Path(parsed)
+ // if library/ was added to the image name, remove it
+ if !strings.HasPrefix(imgRef, "library/") {
+ img.ImageName = strings.TrimPrefix(img.ImageName, "library/")
+ }
+ if digested, ok := parsed.(reference.Digested); ok {
+ img.ImageTag = &tag.ImageTag{
+ TagDigest: string(digested.Digest()),
+ }
+ } else if tagged, ok := parsed.(reference.Tagged); ok {
+ img.ImageTag = &tag.ImageTag{
+ TagName: tagged.Tag(),
+ }
+ }
+ img.original = identifier
+ return &img
+ }
+
+ // if distribution couldn't parse it, fall back to the legacy parsing logic
+ img := ContainerImage{}
+ img.RegistryURL = getRegistryFromIdentifier(identifier)
+ img.ImageAlias, img.ImageName, img.ImageTag = getImageTagFromIdentifier(identifier)
+ img.original = identifier
+ return &img
+}
+
+// String returns the string representation of given ContainerImage
+func (img *ContainerImage) String() string {
+ str := ""
+ if img.ImageAlias != "" {
+ str += img.ImageAlias
+ str += "="
+ }
+ str += img.GetFullNameWithTag()
+ return str
+}
+
+func (img *ContainerImage) GetFullNameWithoutTag() string {
+ str := ""
+ if img.RegistryURL != "" {
+ str += img.RegistryURL + "/"
+ }
+ str += img.ImageName
+ return str
+}
+
+// GetFullNameWithTag returns the complete image slug, including the registry
+// and any tag digest or tag name set for the image.
+func (img *ContainerImage) GetFullNameWithTag() string {
+ str := ""
+ if img.RegistryURL != "" {
+ str += img.RegistryURL + "/"
+ }
+ str += img.ImageName
+ if img.ImageTag != nil {
+ if img.ImageTag.TagName != "" {
+ str += ":"
+ str += img.ImageTag.TagName
+ }
+ if img.ImageTag.TagDigest != "" {
+ str += "@"
+ str += img.ImageTag.TagDigest
+ }
+ }
+ return str
+}
+
+// GetTagWithDigest returns tag name along with any tag digest set for the image
+func (img *ContainerImage) GetTagWithDigest() string {
+ str := ""
+ if img.ImageTag != nil {
+ if img.ImageTag.TagName != "" {
+ str += img.ImageTag.TagName
+ }
+ if img.ImageTag.TagDigest != "" {
+ if str == "" {
+ str += "latest"
+ }
+ str += "@"
+ str += img.ImageTag.TagDigest
+ }
+ }
+ return str
+}
+
+func (img *ContainerImage) Original() string {
+ return img.original
+}
+
+// IsUpdatable checks whether the given image can be updated with newTag while
+// taking tagSpec into account. tagSpec must be given as a semver compatible
+// version spec, i.e. ^1.0 or ~2.1
+func (img *ContainerImage) IsUpdatable(newTag, tagSpec string) bool {
+ return false
+}
+
+// WithTag returns a copy of img with new tag information set
+func (img *ContainerImage) WithTag(newTag *tag.ImageTag) *ContainerImage {
+ nimg := &ContainerImage{}
+ nimg.RegistryURL = img.RegistryURL
+ nimg.ImageName = img.ImageName
+ nimg.ImageTag = newTag
+ nimg.ImageAlias = img.ImageAlias
+ nimg.HelmParamImageName = img.HelmParamImageName
+ nimg.HelmParamImageVersion = img.HelmParamImageVersion
+ return nimg
+}
+
+func (img *ContainerImage) DiffersFrom(other *ContainerImage, checkVersion bool) bool {
+ return img.RegistryURL != other.RegistryURL || img.ImageName != other.ImageName || (checkVersion && img.ImageTag.TagName != other.ImageTag.TagName)
+}
+
+// ContainsImage checks whether img is contained in a list of images
+func (list *ContainerImageList) ContainsImage(img *ContainerImage, checkVersion bool) *ContainerImage {
+ // if there is a KustomizeImage override, check it for a match first
+ if img.KustomizeImage != nil {
+ if kustomizeMatch := list.ContainsImage(img.KustomizeImage, checkVersion); kustomizeMatch != nil {
+ return kustomizeMatch
+ }
+ }
+ for _, image := range *list {
+ if img.ImageName == image.ImageName && image.RegistryURL == img.RegistryURL {
+ if !checkVersion || image.ImageTag.TagName == img.ImageTag.TagName {
+ return image
+ }
+ }
+ }
+ return nil
+}
+
+func (list *ContainerImageList) Originals() []string {
+ results := make([]string, len(*list))
+ for i, img := range *list {
+ results[i] = img.Original()
+ }
+ return results
+}
+
+// String Returns the name of all images as a string, separated using comma
+func (list *ContainerImageList) String() string {
+ imgNameList := make([]string, 0)
+ for _, image := range *list {
+ imgNameList = append(imgNameList, image.String())
+ }
+ return strings.Join(imgNameList, ",")
+}
+
+// Gets the registry URL from an image identifier
+func getRegistryFromIdentifier(identifier string) string {
+ var imageString string
+ comp := strings.Split(identifier, "=")
+ if len(comp) > 1 {
+ imageString = comp[1]
+ } else {
+ imageString = identifier
+ }
+ comp = strings.Split(imageString, "/")
+ if len(comp) > 1 && strings.Contains(comp[0], ".") {
+ return comp[0]
+ } else {
+ return ""
+ }
+}
+
+// Gets the image name and tag from an image identifier
+func getImageTagFromIdentifier(identifier string) (string, string, *tag.ImageTag) {
+ var imageString string
+ var sourceName string
+
+ // The original name is prepended to the image name, separated by =
+ comp := strings.SplitN(identifier, "=", 2)
+ if len(comp) == 2 {
+ sourceName = comp[0]
+ imageString = comp[1]
+ } else {
+ imageString = identifier
+ }
+
+ // Strip any repository identifier from the string
+ comp = strings.Split(imageString, "/")
+ if len(comp) > 1 && strings.Contains(comp[0], ".") {
+ imageString = strings.Join(comp[1:], "/")
+ }
+
+ // We can either have a tag name or a digest reference, or both
+ // jannfis/test-image:0.1
+ // gcr.io/jannfis/test-image:0.1
+ // gcr.io/jannfis/test-image@sha256:abcde
+ // gcr.io/jannfis/test-image:test-tag@sha256:abcde
+ if strings.Contains(imageString, "@") {
+ comp = strings.SplitN(imageString, "@", 2)
+ colonPos := strings.LastIndex(comp[0], ":")
+ slashPos := strings.LastIndex(comp[0], "/")
+ if colonPos > slashPos {
+ // first half (before @) contains image and tag name
+ return sourceName, comp[0][:colonPos], tag.NewImageTag(comp[0][colonPos+1:], time.Unix(0, 0), comp[1])
+ } else {
+ // first half contains image name without tag name
+ return sourceName, comp[0], tag.NewImageTag("", time.Unix(0, 0), comp[1])
+ }
+ } else {
+ comp = strings.SplitN(imageString, ":", 2)
+ if len(comp) != 2 {
+ return sourceName, imageString, nil
+ } else {
+ tagName, tagDigest := getImageDigestFromTag(comp[1])
+ return sourceName, comp[0], tag.NewImageTag(tagName, time.Unix(0, 0), tagDigest)
+ }
+ }
+}
+
+func getImageDigestFromTag(tagStr string) (string, string) {
+ a := strings.Split(tagStr, "@")
+ if len(a) != 2 {
+ return tagStr, ""
+ } else {
+ return a[0], a[1]
+ }
+}
+
+// LogContext returns a log context for the given image, with required fields
+// set to the image's information.
+func (img *ContainerImage) LogContext() *log.LogContext {
+ logCtx := log.WithContext()
+ logCtx.AddField("image_name", img.GetFullNameWithoutTag())
+ logCtx.AddField("image_alias", img.ImageAlias)
+ logCtx.AddField("registry_url", img.RegistryURL)
+ if img.ImageTag != nil {
+ logCtx.AddField("image_tag", img.ImageTag.TagName)
+ logCtx.AddField("image_digest", img.ImageTag.TagDigest)
+ }
+ return logCtx
+}
diff --git a/registry-scanner/pkg/image/image_test.go b/registry-scanner/pkg/image/image_test.go
new file mode 100644
index 0000000..1709461
--- /dev/null
+++ b/registry-scanner/pkg/image/image_test.go
@@ -0,0 +1,226 @@
+package image
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/exp/slices"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag"
+)
+
+func Test_ParseImageTags(t *testing.T) {
+ t.Run("Parse valid image name without registry info", func(t *testing.T) {
+ image := NewFromIdentifier("jannfis/test-image:0.1")
+ assert.Empty(t, image.RegistryURL)
+ assert.Empty(t, image.ImageAlias)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ require.NotNil(t, image.ImageTag)
+ assert.Equal(t, "0.1", image.ImageTag.TagName)
+ assert.Equal(t, "jannfis/test-image:0.1", image.GetFullNameWithTag())
+ assert.Equal(t, "jannfis/test-image", image.GetFullNameWithoutTag())
+ })
+
+ t.Run("Single element image name is unmodified", func(t *testing.T) {
+ image := NewFromIdentifier("test-image")
+ assert.Empty(t, image.RegistryURL)
+ assert.Empty(t, image.ImageAlias)
+ assert.Equal(t, "test-image", image.ImageName)
+ require.Nil(t, image.ImageTag)
+ assert.Equal(t, "test-image", image.GetFullNameWithTag())
+ assert.Equal(t, "test-image", image.GetFullNameWithoutTag())
+ })
+
+ t.Run("library image name is unmodified", func(t *testing.T) {
+ image := NewFromIdentifier("library/test-image")
+ assert.Empty(t, image.RegistryURL)
+ assert.Empty(t, image.ImageAlias)
+ assert.Equal(t, "library/test-image", image.ImageName)
+ require.Nil(t, image.ImageTag)
+ assert.Equal(t, "library/test-image", image.GetFullNameWithTag())
+ assert.Equal(t, "library/test-image", image.GetFullNameWithoutTag())
+ })
+
+ t.Run("Parse valid image name with registry info", func(t *testing.T) {
+ image := NewFromIdentifier("gcr.io/jannfis/test-image:0.1")
+ assert.Equal(t, "gcr.io", image.RegistryURL)
+ assert.Empty(t, image.ImageAlias)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ require.NotNil(t, image.ImageTag)
+ assert.Equal(t, "0.1", image.ImageTag.TagName)
+ assert.Equal(t, "gcr.io/jannfis/test-image:0.1", image.GetFullNameWithTag())
+ assert.Equal(t, "gcr.io/jannfis/test-image", image.GetFullNameWithoutTag())
+ })
+
+ t.Run("Parse valid image name with default registry info", func(t *testing.T) {
+ image := NewFromIdentifier("docker.io/jannfis/test-image:0.1")
+ assert.Equal(t, "docker.io", image.RegistryURL)
+ assert.Empty(t, image.ImageAlias)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ require.NotNil(t, image.ImageTag)
+ assert.Equal(t, "0.1", image.ImageTag.TagName)
+ assert.Equal(t, "docker.io/jannfis/test-image:0.1", image.GetFullNameWithTag())
+ assert.Equal(t, "docker.io/jannfis/test-image", image.GetFullNameWithoutTag())
+ })
+
+ t.Run("Parse valid image name with digest tag", func(t *testing.T) {
+ image := NewFromIdentifier("gcr.io/jannfis/test-image@sha256:abcde")
+ assert.Equal(t, "gcr.io", image.RegistryURL)
+ assert.Empty(t, image.ImageAlias)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ require.NotNil(t, image.ImageTag)
+ assert.Empty(t, image.ImageTag.TagName)
+ assert.Equal(t, "sha256:abcde", image.ImageTag.TagDigest)
+ assert.Equal(t, "latest@sha256:abcde", image.GetTagWithDigest())
+ assert.Equal(t, "gcr.io/jannfis/test-image@sha256:abcde", image.GetFullNameWithTag())
+ assert.Equal(t, "gcr.io/jannfis/test-image", image.GetFullNameWithoutTag())
+ })
+
+ t.Run("Parse valid image name with tag and digest", func(t *testing.T) {
+ image := NewFromIdentifier("gcr.io/jannfis/test-image:test-tag@sha256:abcde")
+ require.NotNil(t, image.ImageTag)
+ assert.Equal(t, "test-tag", image.ImageTag.TagName)
+ assert.Equal(t, "sha256:abcde", image.ImageTag.TagDigest)
+ assert.Equal(t, "test-tag@sha256:abcde", image.GetTagWithDigest())
+ assert.Equal(t, "gcr.io/jannfis/test-image", image.GetFullNameWithoutTag())
+ assert.Equal(t, "gcr.io/jannfis/test-image:test-tag@sha256:abcde", image.GetFullNameWithTag())
+ })
+
+ t.Run("Parse valid image name with source name and registry info", func(t *testing.T) {
+ image := NewFromIdentifier("jannfis/orig-image=gcr.io/jannfis/test-image:0.1")
+ assert.Equal(t, "gcr.io", image.RegistryURL)
+ assert.Equal(t, "jannfis/orig-image", image.ImageAlias)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ require.NotNil(t, image.ImageTag)
+ assert.Equal(t, "0.1", image.ImageTag.TagName)
+ })
+
+ t.Run("Parse valid image name with source name and registry info with port", func(t *testing.T) {
+ image := NewFromIdentifier("ghcr.io:4567/jannfis/orig-image=gcr.io:1234/jannfis/test-image:0.1")
+ assert.Equal(t, "gcr.io:1234", image.RegistryURL)
+ assert.Equal(t, "ghcr.io:4567/jannfis/orig-image", image.ImageAlias)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ require.NotNil(t, image.ImageTag)
+ assert.Equal(t, "0.1", image.ImageTag.TagName)
+ })
+
+ t.Run("Parse image without version source name and registry info", func(t *testing.T) {
+ image := NewFromIdentifier("jannfis/orig-image=gcr.io/jannfis/test-image")
+ assert.Equal(t, "gcr.io", image.RegistryURL)
+ assert.Equal(t, "jannfis/orig-image", image.ImageAlias)
+ assert.Equal(t, "jannfis/test-image", image.ImageName)
+ assert.Nil(t, image.ImageTag)
+ })
+ t.Run("#273 classic-web=registry:5000/classic-web", func(t *testing.T) {
+ image := NewFromIdentifier("classic-web=registry:5000/classic-web")
+ assert.Equal(t, "registry:5000", image.RegistryURL)
+ assert.Equal(t, "classic-web", image.ImageAlias)
+ assert.Equal(t, "classic-web", image.ImageName)
+ assert.Nil(t, image.ImageTag)
+ })
+}
+
+func Test_ImageToString(t *testing.T) {
+ t.Run("Get string representation of full-qualified image name", func(t *testing.T) {
+ imageName := "jannfis/argocd=jannfis/orig-image:0.1"
+ img := NewFromIdentifier(imageName)
+ assert.Equal(t, imageName, img.String())
+ })
+ t.Run("Get string representation of full-qualified image name with registry", func(t *testing.T) {
+ imageName := "jannfis/argocd=gcr.io/jannfis/orig-image:0.1"
+ img := NewFromIdentifier(imageName)
+ assert.Equal(t, imageName, img.String())
+ })
+ t.Run("Get string representation of full-qualified image name with registry", func(t *testing.T) {
+ imageName := "jannfis/argocd=gcr.io/jannfis/orig-image"
+ img := NewFromIdentifier(imageName)
+ assert.Equal(t, imageName, img.String())
+ })
+ t.Run("Get original value", func(t *testing.T) {
+ imageName := "invalid==foo"
+ img := NewFromIdentifier(imageName)
+ assert.Equal(t, imageName, img.Original())
+ })
+}
+
+func Test_WithTag(t *testing.T) {
+ t.Run("Get string representation of full-qualified image name", func(t *testing.T) {
+ imageName := "jannfis/argocd=jannfis/orig-image:0.1"
+ nimageName := "jannfis/argocd=jannfis/orig-image:0.2"
+ oImg := NewFromIdentifier(imageName)
+ nImg := oImg.WithTag(tag.NewImageTag("0.2", time.Unix(0, 0), ""))
+ assert.Equal(t, nimageName, nImg.String())
+ })
+}
+
+func Test_ContainerList(t *testing.T) {
+ t.Run("Test whether image is contained in list", func(t *testing.T) {
+ images := make(ContainerImageList, 0)
+ image_names := []string{"a/a:0.1", "a/b:1.2", "x/y=foo.bar/a/c:0.23"}
+ for _, n := range image_names {
+ images = append(images, NewFromIdentifier(n))
+ }
+ withKustomizeOverride := NewFromIdentifier("k1/k2:k3")
+ withKustomizeOverride.KustomizeImage = images[0]
+ images = append(images, withKustomizeOverride)
+
+ assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[0]), false))
+ assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[1]), false))
+ assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[2]), false))
+ assert.Nil(t, images.ContainsImage(NewFromIdentifier("foo/bar"), false))
+
+ imageMatch := images.ContainsImage(withKustomizeOverride, false)
+ assert.Equal(t, images[0], imageMatch)
+ })
+}
+
+func Test_getImageDigestFromTag(t *testing.T) {
+ tagAndDigest := "test-tag@sha256:abcde"
+ tagName, tagDigest := getImageDigestFromTag(tagAndDigest)
+ assert.Equal(t, "test-tag", tagName)
+ assert.Equal(t, "sha256:abcde", tagDigest)
+
+ tagAndDigest = "test-tag"
+ tagName, tagDigest = getImageDigestFromTag(tagAndDigest)
+ assert.Equal(t, "test-tag", tagName)
+ assert.Empty(t, tagDigest)
+}
+
+func Test_ContainerImageList_String_Originals(t *testing.T) {
+ images := make(ContainerImageList, 0)
+ originals := []string{}
+
+ assert.Equal(t, "", images.String())
+ assert.True(t, slices.Equal(originals, images.Originals()))
+
+ images = append(images, NewFromIdentifier("foo/bar:0.1"))
+ originals = append(originals, "foo/bar:0.1")
+ assert.Equal(t, "foo/bar:0.1", images.String())
+ assert.True(t, slices.Equal(originals, images.Originals()))
+
+ images = append(images, NewFromIdentifier("alias=foo/bar:0.2"))
+ originals = append(originals, "alias=foo/bar:0.2")
+ assert.Equal(t, "foo/bar:0.1,alias=foo/bar:0.2", images.String())
+ assert.True(t, slices.Equal(originals, images.Originals()))
+}
+
+func TestContainerImage_DiffersFrom(t *testing.T) {
+ foo1 := NewFromIdentifier("x/foo:1")
+ foo2 := NewFromIdentifier("x/foo:2")
+ bar1 := NewFromIdentifier("x/bar:1")
+ bar1WithRegistry := NewFromIdentifier("docker.io/x/bar:1")
+
+ assert.False(t, foo1.DiffersFrom(foo1, true))
+ assert.False(t, foo1.DiffersFrom(foo2, false))
+ assert.True(t, foo1.DiffersFrom(foo2, true))
+
+ assert.True(t, foo1.DiffersFrom(bar1, false))
+ assert.True(t, bar1.DiffersFrom(foo1, false))
+ assert.True(t, foo1.DiffersFrom(bar1, true))
+ assert.True(t, bar1.DiffersFrom(foo1, true))
+ assert.True(t, bar1.DiffersFrom(bar1WithRegistry, false))
+
+ assert.False(t, foo1.IsUpdatable("0.1", "^1.0"))
+}
diff --git a/registry-scanner/pkg/image/kustomize.go b/registry-scanner/pkg/image/kustomize.go
new file mode 100644
index 0000000..ef7c88b
--- /dev/null
+++ b/registry-scanner/pkg/image/kustomize.go
@@ -0,0 +1,39 @@
+package image
+
+import (
+ "strings"
+)
+
+// Shamelessly ripped from ArgoCD CLI code
+
+type KustomizeImage string
+
+func (i KustomizeImage) delim() string {
+ for _, d := range []string{"=", ":", "@"} {
+ if strings.Contains(string(i), d) {
+ return d
+ }
+ }
+ return ":"
+}
+
+// if the image name matches (i.e. up to the first delimiter)
+func (i KustomizeImage) Match(j KustomizeImage) bool {
+ delim := j.delim()
+ if !strings.Contains(string(j), delim) {
+ return false
+ }
+ return strings.HasPrefix(string(i), strings.Split(string(j), delim)[0])
+}
+
+type KustomizeImages []KustomizeImage
+
+// find the image or -1
+func (images KustomizeImages) Find(image KustomizeImage) int {
+ for i, a := range images {
+ if a.Match(image) {
+ return i
+ }
+ }
+ return -1
+}
diff --git a/registry-scanner/pkg/image/kustomize_test.go b/registry-scanner/pkg/image/kustomize_test.go
new file mode 100644
index 0000000..98dede9
--- /dev/null
+++ b/registry-scanner/pkg/image/kustomize_test.go
@@ -0,0 +1,26 @@
+package image
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_KustomizeImages_Find(t *testing.T) {
+ images := KustomizeImages{
+ "a/b:1.0",
+ "a/b@sha256:aabb",
+ "a/b:latest@sha256:aabb",
+ "x/y=busybox",
+ "x/y=foo.bar/a/c:0.23",
+ }
+ for _, image := range images {
+ assert.True(t, images.Find(image) >= 0)
+ }
+ for _, image := range []string{"a/b:2", "x/y=foo.bar"} {
+ assert.True(t, images.Find(KustomizeImage(image)) >= 0)
+ }
+ for _, image := range []string{"a/b", "x", "x/y"} {
+ assert.Equal(t, -1, images.Find(KustomizeImage(image)))
+ }
+}
diff --git a/registry-scanner/pkg/image/matchfunc.go b/registry-scanner/pkg/image/matchfunc.go
new file mode 100644
index 0000000..036e6fb
--- /dev/null
+++ b/registry-scanner/pkg/image/matchfunc.go
@@ -0,0 +1,27 @@
+package image
+
+import (
+ "regexp"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
+)
+
+// MatchFuncAny matches any pattern, i.e. always returns true
+func MatchFuncAny(tagName string, args interface{}) bool {
+ return true
+}
+
+// MatchFuncNone matches no pattern, i.e. always returns false
+func MatchFuncNone(tagName string, args interface{}) bool {
+ return false
+}
+
+// MatchFuncRegexp matches the tagName against regexp pattern and returns the result
+func MatchFuncRegexp(tagName string, args interface{}) bool {
+ pattern, ok := args.(*regexp.Regexp)
+ if !ok {
+ log.Errorf("args is not a RegExp")
+ return false
+ }
+ return pattern.Match([]byte(tagName))
+}
diff --git a/registry-scanner/pkg/image/matchfunc_test.go b/registry-scanner/pkg/image/matchfunc_test.go
new file mode 100644
index 0000000..11929b1
--- /dev/null
+++ b/registry-scanner/pkg/image/matchfunc_test.go
@@ -0,0 +1,27 @@
+package image
+
+import (
+ "regexp"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_MatchFuncAny(t *testing.T) {
+ assert.True(t, MatchFuncAny("whatever", nil))
+}
+
+func Test_MatchFuncNone(t *testing.T) {
+ assert.False(t, MatchFuncNone("whatever", nil))
+}
+
+func Test_MatchFuncRegexp(t *testing.T) {
+ t.Run("Test with valid expression", func(t *testing.T) {
+ re := regexp.MustCompile("[a-z]+")
+ assert.True(t, MatchFuncRegexp("lemon", re))
+ assert.False(t, MatchFuncRegexp("31337", re))
+ })
+ t.Run("Test with invalid type", func(t *testing.T) {
+ assert.False(t, MatchFuncRegexp("lemon", "[a-z]+"))
+ })
+}
diff --git a/registry-scanner/pkg/image/options.go b/registry-scanner/pkg/image/options.go
new file mode 100644
index 0000000..3c599dc
--- /dev/null
+++ b/registry-scanner/pkg/image/options.go
@@ -0,0 +1,296 @@
+package image
+
+import (
+ "fmt"
+ "regexp"
+ "runtime"
+ "strings"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/common"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options"
+)
+
+// GetParameterHelmImageName gets the value for image-name option for the image
+// from a set of annotations
+func (img *ContainerImage) GetParameterHelmImageName(annotations map[string]string) string {
+ key := fmt.Sprintf(common.HelmParamImageNameAnnotation, img.normalizedSymbolicName())
+ val, ok := annotations[key]
+ if !ok {
+ return ""
+ }
+ return val
+}
+
+// GetParameterHelmImageTag gets the value for image-tag option for the image
+// from a set of annotations
+func (img *ContainerImage) GetParameterHelmImageTag(annotations map[string]string) string {
+ key := fmt.Sprintf(common.HelmParamImageTagAnnotation, img.normalizedSymbolicName())
+ val, ok := annotations[key]
+ if !ok {
+ return ""
+ }
+ return val
+}
+
+// GetParameterHelmImageSpec gets the value for image-spec option for the image
+// from a set of annotations
+func (img *ContainerImage) GetParameterHelmImageSpec(annotations map[string]string) string {
+ key := fmt.Sprintf(common.HelmParamImageSpecAnnotation, img.normalizedSymbolicName())
+ val, ok := annotations[key]
+ if !ok {
+ return ""
+ }
+ return val
+}
+
+// GetParameterKustomizeImageName gets the value for image-spec option for the
+// image from a set of annotations
+func (img *ContainerImage) GetParameterKustomizeImageName(annotations map[string]string) string {
+ key := fmt.Sprintf(common.KustomizeApplicationNameAnnotation, img.normalizedSymbolicName())
+ val, ok := annotations[key]
+ if !ok {
+ return ""
+ }
+ return val
+}
+
+// HasForceUpdateOptionAnnotation gets the value for force-update option for the
+// image from a set of annotations
+func (img *ContainerImage) HasForceUpdateOptionAnnotation(annotations map[string]string) bool {
+ forceUpdateAnnotations := []string{
+ fmt.Sprintf(common.ForceUpdateOptionAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWideForceUpdateOptionAnnotation,
+ }
+ var forceUpdateVal = ""
+ for _, key := range forceUpdateAnnotations {
+ if val, ok := annotations[key]; ok {
+ forceUpdateVal = val
+ break
+ }
+ }
+ return forceUpdateVal == "true"
+}
+
+// GetParameterSort gets and validates the value for the sort option for the
+// image from a set of annotations
+func (img *ContainerImage) GetParameterUpdateStrategy(annotations map[string]string) UpdateStrategy {
+ updateStrategyAnnotations := []string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWideUpdateStrategyAnnotation,
+ }
+ var updateStrategyVal = ""
+ for _, key := range updateStrategyAnnotations {
+ if val, ok := annotations[key]; ok {
+ updateStrategyVal = val
+ break
+ }
+ }
+ logCtx := img.LogContext()
+ if updateStrategyVal == "" {
+ logCtx.Tracef("No sort option found")
+ // Default is sort by version
+ return StrategySemVer
+ }
+ logCtx.Tracef("Found update strategy %s", updateStrategyVal)
+ return img.ParseUpdateStrategy(updateStrategyVal)
+}
+
+func (img *ContainerImage) ParseUpdateStrategy(val string) UpdateStrategy {
+ logCtx := img.LogContext()
+ switch strings.ToLower(val) {
+ case "semver":
+ return StrategySemVer
+ case "latest":
+ logCtx.Warnf("\"latest\" strategy has been renamed to \"newest-build\". Please switch to the new convention as support for the old naming convention will be removed in future versions.")
+ fallthrough
+ case "newest-build":
+ return StrategyNewestBuild
+ case "name":
+ logCtx.Warnf("\"name\" strategy has been renamed to \"alphabetical\". Please switch to the new convention as support for the old naming convention will be removed in future versions.")
+ fallthrough
+ case "alphabetical":
+ return StrategyAlphabetical
+ case "digest":
+ return StrategyDigest
+ default:
+ logCtx.Warnf("Unknown sort option %s -- using semver", val)
+ return StrategySemVer
+ }
+}
+
+// GetParameterMatch returns the match function and pattern to use for matching
+// tag names. If an invalid option is found, it returns MatchFuncNone as the
+// default, to prevent accidental matches.
+func (img *ContainerImage) GetParameterMatch(annotations map[string]string) (MatchFuncFn, interface{}) {
+ allowTagsAnnotations := []string{
+ fmt.Sprintf(common.AllowTagsOptionAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWideAllowTagsOptionAnnotation,
+ }
+ var allowTagsVal = ""
+ for _, key := range allowTagsAnnotations {
+ if val, ok := annotations[key]; ok {
+ allowTagsVal = val
+ break
+ }
+ }
+ logCtx := img.LogContext()
+ if allowTagsVal == "" {
+ // The old match-tag annotation is deprecated and will be subject to removal
+ // in a future version.
+ key := fmt.Sprintf(common.OldMatchOptionAnnotation, img.normalizedSymbolicName())
+ val, ok := annotations[key]
+ if ok {
+ logCtx.Warnf("The 'tag-match' annotation is deprecated and subject to removal. Please use 'allow-tags' annotation instead.")
+ allowTagsVal = val
+ }
+ }
+ if allowTagsVal == "" {
+ logCtx.Tracef("No match annotation found")
+ return MatchFuncAny, ""
+ }
+ return img.ParseMatchfunc(allowTagsVal)
+}
+
+// ParseMatchfunc returns a matcher function and its argument from given value
+func (img *ContainerImage) ParseMatchfunc(val string) (MatchFuncFn, interface{}) {
+ logCtx := img.LogContext()
+
+ // The special value "any" doesn't take any parameter
+ if strings.ToLower(val) == "any" {
+ return MatchFuncAny, nil
+ }
+
+ opt := strings.SplitN(val, ":", 2)
+ if len(opt) != 2 {
+ logCtx.Warnf("Invalid match option syntax '%s', ignoring", val)
+ return MatchFuncNone, nil
+ }
+ switch strings.ToLower(opt[0]) {
+ case "regexp":
+ re, err := regexp.Compile(opt[1])
+ if err != nil {
+ logCtx.Warnf("Could not compile regexp '%s'", opt[1])
+ return MatchFuncNone, nil
+ }
+ return MatchFuncRegexp, re
+ default:
+ logCtx.Warnf("Unknown match function: %s", opt[0])
+ return MatchFuncNone, nil
+ }
+}
+
+// GetParameterPullSecret retrieves an image's pull secret credentials
+func (img *ContainerImage) GetParameterPullSecret(annotations map[string]string) *CredentialSource {
+ pullSecretAnnotations := []string{
+ fmt.Sprintf(common.PullSecretAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWidePullSecretAnnotation,
+ }
+ var pullSecretVal = ""
+ for _, key := range pullSecretAnnotations {
+ if val, ok := annotations[key]; ok {
+ pullSecretVal = val
+ break
+ }
+ }
+ logCtx := img.LogContext()
+ if pullSecretVal == "" {
+ logCtx.Tracef("No pull-secret annotation found")
+ return nil
+ }
+ credSrc, err := ParseCredentialSource(pullSecretVal, false)
+ if err != nil {
+ logCtx.Warnf("Invalid credential reference specified: %s", pullSecretVal)
+ return nil
+ }
+ return credSrc
+}
+
+// GetParameterIgnoreTags retrieves a list of tags to ignore from a comma-separated string
+func (img *ContainerImage) GetParameterIgnoreTags(annotations map[string]string) []string {
+ ignoreTagsAnnotations := []string{
+ fmt.Sprintf(common.IgnoreTagsOptionAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWideIgnoreTagsOptionAnnotation,
+ }
+ var ignoreTagsVal = ""
+ for _, key := range ignoreTagsAnnotations {
+ if val, ok := annotations[key]; ok {
+ ignoreTagsVal = val
+ break
+ }
+ }
+ logCtx := img.LogContext()
+ if ignoreTagsVal == "" {
+ logCtx.Tracef("No ignore-tags annotation found")
+ return nil
+ }
+ ignoreList := make([]string, 0)
+ tags := strings.Split(strings.TrimSpace(ignoreTagsVal), ",")
+ for _, tag := range tags {
+ // We ignore empty tags
+ trimmed := strings.TrimSpace(tag)
+ if trimmed != "" {
+ ignoreList = append(ignoreList, trimmed)
+ }
+ }
+ return ignoreList
+}
+
+// GetPlatformOptions sets up platform constraints for an image. If no platform
+// is specified in the annotations, we restrict the platform for images to the
+// platform we're executed on unless unrestricted is set to true, in which case
+// we do not setup a platform restriction if no platform annotation is found.
+func (img *ContainerImage) GetPlatformOptions(annotations map[string]string, unrestricted bool) *options.ManifestOptions {
+ logCtx := img.LogContext()
+ var opts *options.ManifestOptions = options.NewManifestOptions()
+ key := fmt.Sprintf(common.PlatformsAnnotation, img.normalizedSymbolicName())
+ val, ok := annotations[key]
+ if !ok {
+ if !unrestricted {
+ os := runtime.GOOS
+ arch := runtime.GOARCH
+ variant := ""
+ if strings.Contains(runtime.GOARCH, "/") {
+ a := strings.SplitN(runtime.GOARCH, "/", 2)
+ arch = a[0]
+ variant = a[1]
+ }
+ logCtx.Tracef("Using runtime platform constraint %s", options.PlatformKey(os, arch, variant))
+ opts = opts.WithPlatform(os, arch, variant)
+ }
+ } else {
+ platforms := strings.Split(val, ",")
+ for _, ps := range platforms {
+ pt := strings.TrimSpace(ps)
+ os, arch, variant, err := ParsePlatform(pt)
+ if err != nil {
+ // If the platform identifier could not be parsed, we set the
+ // constraint intentionally to the invalid value so we don't
+ // end up updating to the wrong architecture possibly.
+ os = ps
+ logCtx.Warnf("could not parse platform identifier '%v': invalid format", pt)
+ }
+ logCtx.Tracef("Adding platform constraint %s", options.PlatformKey(os, arch, variant))
+ opts = opts.WithPlatform(os, arch, variant)
+ }
+ }
+
+ return opts
+}
+
+func ParsePlatform(platformID string) (string, string, string, error) {
+ p := strings.SplitN(platformID, "/", 3)
+ if len(p) < 2 {
+ return "", "", "", fmt.Errorf("could not parse platform constraint '%s'", platformID)
+ }
+ os := p[0]
+ arch := p[1]
+ variant := ""
+ if len(p) == 3 {
+ variant = p[2]
+ }
+ return os, arch, variant, nil
+}
+
+func (img *ContainerImage) normalizedSymbolicName() string {
+ return strings.ReplaceAll(img.ImageAlias, "/", "_")
+}
diff --git a/registry-scanner/pkg/image/options_test.go b/registry-scanner/pkg/image/options_test.go
new file mode 100644
index 0000000..c1c7613
--- /dev/null
+++ b/registry-scanner/pkg/image/options_test.go
@@ -0,0 +1,493 @@
+package image
+
+import (
+ "fmt"
+ "regexp"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/common"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options"
+)
+
+func Test_GetHelmOptions(t *testing.T) {
+ t.Run("Get Helm parameter for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.HelmParamImageNameAnnotation, "dummy"): "release.name",
+ fmt.Sprintf(common.HelmParamImageTagAnnotation, "dummy"): "release.tag",
+ fmt.Sprintf(common.HelmParamImageSpecAnnotation, "dummy"): "release.image",
+ }
+
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ paramName := img.GetParameterHelmImageName(annotations)
+ paramTag := img.GetParameterHelmImageTag(annotations)
+ paramSpec := img.GetParameterHelmImageSpec(annotations)
+ assert.Equal(t, "release.name", paramName)
+ assert.Equal(t, "release.tag", paramTag)
+ assert.Equal(t, "release.image", paramSpec)
+ })
+
+ t.Run("Get Helm parameter for non-configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.HelmParamImageNameAnnotation, "dummy"): "release.name",
+ fmt.Sprintf(common.HelmParamImageTagAnnotation, "dummy"): "release.tag",
+ fmt.Sprintf(common.HelmParamImageSpecAnnotation, "dummy"): "release.image",
+ }
+
+ img := NewFromIdentifier("foo=foo/bar:1.12")
+ paramName := img.GetParameterHelmImageName(annotations)
+ paramTag := img.GetParameterHelmImageTag(annotations)
+ paramSpec := img.GetParameterHelmImageSpec(annotations)
+ assert.Equal(t, "", paramName)
+ assert.Equal(t, "", paramTag)
+ assert.Equal(t, "", paramSpec)
+ })
+
+ t.Run("Get Helm parameter for configured application with normalized name", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.HelmParamImageNameAnnotation, "foo_dummy"): "release.name",
+ fmt.Sprintf(common.HelmParamImageTagAnnotation, "foo_dummy"): "release.tag",
+ fmt.Sprintf(common.HelmParamImageSpecAnnotation, "foo_dummy"): "release.image",
+ }
+
+ img := NewFromIdentifier("foo/dummy=foo/bar:1.12")
+ paramName := img.GetParameterHelmImageName(annotations)
+ paramTag := img.GetParameterHelmImageTag(annotations)
+ paramSpec := img.GetParameterHelmImageSpec(annotations)
+ assert.Equal(t, "release.name", paramName)
+ assert.Equal(t, "release.tag", paramTag)
+ assert.Equal(t, "release.image", paramSpec)
+ })
+}
+
+func Test_GetKustomizeOptions(t *testing.T) {
+ t.Run("Get Kustomize parameter for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.KustomizeApplicationNameAnnotation, "dummy"): "argoproj/argo-cd",
+ }
+
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ paramName := img.GetParameterKustomizeImageName(annotations)
+ assert.Equal(t, "argoproj/argo-cd", paramName)
+
+ img = NewFromIdentifier("dummy2=foo2/bar2:1.12")
+ paramName = img.GetParameterKustomizeImageName(annotations)
+ assert.Equal(t, "", paramName)
+ })
+}
+
+func Test_GetSortOption(t *testing.T) {
+ t.Run("Get update strategy semver for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "semver",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategySemVer, sortMode)
+ })
+
+ t.Run("Use update strategy newest-build for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "newest-build",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyNewestBuild, sortMode)
+ })
+
+ t.Run("Get update strategy date for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "latest",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyNewestBuild, sortMode)
+ })
+
+ t.Run("Get update strategy name for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "name",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyAlphabetical, sortMode)
+ })
+
+ t.Run("Use update strategy alphabetical for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "alphabetical",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyAlphabetical, sortMode)
+ })
+
+ t.Run("Get update strategy option configured application because of invalid option", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "invalid",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategySemVer, sortMode)
+ })
+
+ t.Run("Get update strategy option configured application because of option not set", func(t *testing.T) {
+ annotations := map[string]string{}
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategySemVer, sortMode)
+ })
+
+ t.Run("Prefer update strategy option from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "alphabetical",
+ common.ApplicationWideUpdateStrategyAnnotation: "newest-build",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyAlphabetical, sortMode)
+ })
+
+ t.Run("Get update strategy option from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideUpdateStrategyAnnotation: "newest-build",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyNewestBuild, sortMode)
+ })
+
+ t.Run("Get update strategy option digest from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideUpdateStrategyAnnotation: "digest",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyDigest, sortMode)
+ })
+}
+
+func Test_GetMatchOption(t *testing.T) {
+ t.Run("Get regexp match option for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): "regexp:a-z",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, matchArgs := img.GetParameterMatch(annotations)
+ require.NotNil(t, matchFunc)
+ require.NotNil(t, matchArgs)
+ assert.IsType(t, &regexp.Regexp{}, matchArgs)
+ })
+
+ t.Run("Get regexp match option for configured application with invalid expression", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): `regexp:/foo\`,
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, matchArgs := img.GetParameterMatch(annotations)
+ require.NotNil(t, matchFunc)
+ require.Nil(t, matchArgs)
+ })
+
+ t.Run("Get invalid match option for configured application", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): "invalid",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, matchArgs := img.GetParameterMatch(annotations)
+ require.NotNil(t, matchFunc)
+ require.Equal(t, false, matchFunc("", nil))
+ assert.Nil(t, matchArgs)
+ })
+
+ t.Run("No match option for configured application", func(t *testing.T) {
+ annotations := map[string]string{}
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, matchArgs := img.GetParameterMatch(annotations)
+ require.NotNil(t, matchFunc)
+ require.Equal(t, true, matchFunc("", nil))
+ assert.Equal(t, "", matchArgs)
+ })
+
+ t.Run("Prefer match option from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): "regexp:^[0-9]",
+ common.ApplicationWideAllowTagsOptionAnnotation: "regexp:^v",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, matchArgs := img.GetParameterMatch(annotations)
+ require.NotNil(t, matchFunc)
+ require.NotNil(t, matchArgs)
+ assert.IsType(t, &regexp.Regexp{}, matchArgs)
+ assert.True(t, matchFunc("0.0.1", matchArgs))
+ assert.False(t, matchFunc("v0.0.1", matchArgs))
+ })
+
+ t.Run("Get match option from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideAllowTagsOptionAnnotation: "regexp:^v",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, matchArgs := img.GetParameterMatch(annotations)
+ require.NotNil(t, matchFunc)
+ require.NotNil(t, matchArgs)
+ assert.IsType(t, &regexp.Regexp{}, matchArgs)
+ assert.False(t, matchFunc("0.0.1", matchArgs))
+ assert.True(t, matchFunc("v0.0.1", matchArgs))
+ })
+}
+
+func Test_GetSecretOption(t *testing.T) {
+ t.Run("Get cred source from annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "pullsecret:foo/bar",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.NotNil(t, credSrc)
+ assert.Equal(t, CredentialSourcePullSecret, credSrc.Type)
+ assert.Equal(t, "foo", credSrc.SecretNamespace)
+ assert.Equal(t, "bar", credSrc.SecretName)
+ assert.Equal(t, ".dockerconfigjson", credSrc.SecretField)
+ })
+
+ t.Run("Invalid reference in annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "foo/bar",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.Nil(t, credSrc)
+ })
+
+ t.Run("Missing pull secret in annotation", func(t *testing.T) {
+ annotations := map[string]string{}
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.Nil(t, credSrc)
+ })
+
+ t.Run("Prefer cred source from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "pullsecret:image/specific",
+ common.ApplicationWidePullSecretAnnotation: "pullsecret:app/wide",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.NotNil(t, credSrc)
+ assert.Equal(t, CredentialSourcePullSecret, credSrc.Type)
+ assert.Equal(t, "image", credSrc.SecretNamespace)
+ assert.Equal(t, "specific", credSrc.SecretName)
+ assert.Equal(t, ".dockerconfigjson", credSrc.SecretField)
+ })
+
+ t.Run("Get cred source from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWidePullSecretAnnotation: "pullsecret:app/wide",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.NotNil(t, credSrc)
+ assert.Equal(t, CredentialSourcePullSecret, credSrc.Type)
+ assert.Equal(t, "app", credSrc.SecretNamespace)
+ assert.Equal(t, "wide", credSrc.SecretName)
+ assert.Equal(t, ".dockerconfigjson", credSrc.SecretField)
+ })
+}
+
+func Test_GetIgnoreTags(t *testing.T) {
+ t.Run("Get list of tags to ignore from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.IgnoreTagsOptionAnnotation, "dummy"): "tag1, ,tag2, tag3 , tag4",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ tags := img.GetParameterIgnoreTags(annotations)
+ require.Len(t, tags, 4)
+ assert.Equal(t, "tag1", tags[0])
+ assert.Equal(t, "tag2", tags[1])
+ assert.Equal(t, "tag3", tags[2])
+ assert.Equal(t, "tag4", tags[3])
+ })
+
+ t.Run("No tags to ignore from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{}
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ tags := img.GetParameterIgnoreTags(annotations)
+ require.Nil(t, tags)
+ })
+
+ t.Run("Prefer list of tags to ignore from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.IgnoreTagsOptionAnnotation, "dummy"): "tag1, tag2",
+ common.ApplicationWideIgnoreTagsOptionAnnotation: "tag3, tag4",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ tags := img.GetParameterIgnoreTags(annotations)
+ require.Len(t, tags, 2)
+ assert.Equal(t, "tag1", tags[0])
+ assert.Equal(t, "tag2", tags[1])
+ })
+
+ t.Run("Get list of tags to ignore from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideIgnoreTagsOptionAnnotation: "tag3, tag4",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ tags := img.GetParameterIgnoreTags(annotations)
+ require.Len(t, tags, 2)
+ assert.Equal(t, "tag3", tags[0])
+ assert.Equal(t, "tag4", tags[1])
+ })
+}
+
+func Test_HasForceUpdateOptionAnnotation(t *testing.T) {
+ t.Run("Get force-update option from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.ForceUpdateOptionAnnotation, "dummy"): "true",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ forceUpdate := img.HasForceUpdateOptionAnnotation(annotations)
+ assert.True(t, forceUpdate)
+ })
+
+ t.Run("Prefer force-update option from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.ForceUpdateOptionAnnotation, "dummy"): "true",
+ common.ApplicationWideForceUpdateOptionAnnotation: "false",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ forceUpdate := img.HasForceUpdateOptionAnnotation(annotations)
+ assert.True(t, forceUpdate)
+ })
+
+ t.Run("Get force-update option from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideForceUpdateOptionAnnotation: "false",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ forceUpdate := img.HasForceUpdateOptionAnnotation(annotations)
+ assert.False(t, forceUpdate)
+ })
+}
+
+func Test_GetPlatformOptions(t *testing.T) {
+ t.Run("Empty platform options with restriction", func(t *testing.T) {
+ annotations := map[string]string{}
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ opts := img.GetPlatformOptions(annotations, false)
+ os := runtime.GOOS
+ arch := runtime.GOARCH
+ platform := opts.Platforms()[0]
+ slashCount := strings.Count(platform, "/")
+ if slashCount == 1 {
+ assert.True(t, opts.WantsPlatform(os, arch, ""))
+ assert.True(t, opts.WantsPlatform(os, arch, "invalid"))
+ } else if slashCount == 2 {
+ assert.False(t, opts.WantsPlatform(os, arch, ""))
+ assert.False(t, opts.WantsPlatform(os, arch, "invalid"))
+ } else {
+ t.Fatal("invalid platform options ", platform)
+ }
+ })
+ t.Run("Empty platform options without restriction", func(t *testing.T) {
+ annotations := map[string]string{}
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ opts := img.GetPlatformOptions(annotations, true)
+ os := runtime.GOOS
+ arch := runtime.GOARCH
+ assert.True(t, opts.WantsPlatform(os, arch, ""))
+ assert.True(t, opts.WantsPlatform(os, arch, "invalid"))
+ assert.True(t, opts.WantsPlatform("windows", "amd64", ""))
+ })
+ t.Run("Single platform without variant requested", func(t *testing.T) {
+ os := "linux"
+ arch := "arm64"
+ variant := "v8"
+ annotations := map[string]string{
+ fmt.Sprintf(common.PlatformsAnnotation, "dummy"): options.PlatformKey(os, arch, variant),
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ opts := img.GetPlatformOptions(annotations, false)
+ assert.True(t, opts.WantsPlatform(os, arch, variant))
+ assert.False(t, opts.WantsPlatform(os, arch, "invalid"))
+ })
+ t.Run("Single platform with variant requested", func(t *testing.T) {
+ os := "linux"
+ arch := "arm"
+ variant := "v6"
+ annotations := map[string]string{
+ fmt.Sprintf(common.PlatformsAnnotation, "dummy"): options.PlatformKey(os, arch, variant),
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ opts := img.GetPlatformOptions(annotations, false)
+ assert.True(t, opts.WantsPlatform(os, arch, variant))
+ assert.False(t, opts.WantsPlatform(os, arch, ""))
+ assert.False(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, ""))
+ assert.False(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, variant))
+ })
+ t.Run("Multiple platforms requested", func(t *testing.T) {
+ os := "linux"
+ arch := "arm"
+ variant := "v6"
+ annotations := map[string]string{
+ fmt.Sprintf(common.PlatformsAnnotation, "dummy"): options.PlatformKey(os, arch, variant) + ", " + options.PlatformKey(runtime.GOOS, runtime.GOARCH, ""),
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ opts := img.GetPlatformOptions(annotations, false)
+ assert.True(t, opts.WantsPlatform(os, arch, variant))
+ assert.True(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, ""))
+ assert.False(t, opts.WantsPlatform(os, arch, ""))
+ assert.True(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, variant))
+ })
+ t.Run("Invalid platform requested", func(t *testing.T) {
+ os := "linux"
+ arch := "arm"
+ variant := "v6"
+ annotations := map[string]string{
+ fmt.Sprintf(common.PlatformsAnnotation, "dummy"): "invalid",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ opts := img.GetPlatformOptions(annotations, false)
+ assert.False(t, opts.WantsPlatform(os, arch, variant))
+ assert.False(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, ""))
+ assert.False(t, opts.WantsPlatform(os, arch, ""))
+ assert.False(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, variant))
+ })
+}
+
+func Test_ContainerImage_ParseMatchfunc(t *testing.T) {
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, pattern := img.ParseMatchfunc("any")
+ assert.True(t, matchFunc("MatchFuncAny any tag name", pattern))
+ assert.Nil(t, pattern)
+
+ matchFunc, pattern = img.ParseMatchfunc("ANY")
+ assert.True(t, matchFunc("MatchFuncAny any tag name", pattern))
+ assert.Nil(t, pattern)
+
+ matchFunc, pattern = img.ParseMatchfunc("other")
+ assert.False(t, matchFunc("MatchFuncNone any tag name", pattern))
+ assert.Nil(t, pattern)
+
+ matchFunc, pattern = img.ParseMatchfunc("not-regexp:a-z")
+ assert.False(t, matchFunc("MatchFuncNone any tag name", pattern))
+ assert.Nil(t, pattern)
+
+ matchFunc, pattern = img.ParseMatchfunc("regexp:[aA-zZ]")
+ assert.True(t, matchFunc("MatchFuncRegexp-tag-name", pattern))
+ compiledRegexp, _ := regexp.Compile("[aA-zZ]")
+ assert.Equal(t, compiledRegexp, pattern)
+
+ matchFunc, pattern = img.ParseMatchfunc("RegExp:[aA-zZ]")
+ assert.True(t, matchFunc("MatchFuncRegexp-tag-name", pattern))
+ compiledRegexp, _ = regexp.Compile("[aA-zZ]")
+ assert.Equal(t, compiledRegexp, pattern)
+
+ matchFunc, pattern = img.ParseMatchfunc("regexp:[aA-zZ") //invalid regexp: missing end ]
+ assert.False(t, matchFunc("MatchFuncNone-tag-name", pattern))
+ assert.Nil(t, pattern)
+}
diff --git a/registry-scanner/pkg/image/version.go b/registry-scanner/pkg/image/version.go
new file mode 100644
index 0000000..97437bd
--- /dev/null
+++ b/registry-scanner/pkg/image/version.go
@@ -0,0 +1,220 @@
+package image
+
+import (
+ "path/filepath"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag"
+
+ "github.com/Masterminds/semver/v3"
+)
+
+// VersionSortMode defines the method to sort a list of tags
+type UpdateStrategy int
+
+const (
+ // VersionSortSemVer sorts tags using semver sorting (the default)
+ StrategySemVer UpdateStrategy = 0
+ // VersionSortLatest sorts tags after their creation date
+ StrategyNewestBuild UpdateStrategy = 1
+ // VersionSortName sorts tags alphabetically by name
+ StrategyAlphabetical UpdateStrategy = 2
+ // VersionSortDigest uses latest digest of an image
+ StrategyDigest UpdateStrategy = 3
+)
+
+func (us UpdateStrategy) String() string {
+ switch us {
+ case StrategySemVer:
+ return "semver"
+ case StrategyNewestBuild:
+ return "newest-build"
+ case StrategyAlphabetical:
+ return "alphabetical"
+ case StrategyDigest:
+ return "digest"
+ }
+
+ return "unknown"
+}
+
+// ConstraintMatchMode defines how the constraint should be matched
+type ConstraintMatchMode int
+
+const (
+ // ConstraintMatchSemVer uses semver to match a constraint
+ ConstraintMatchSemver ConstraintMatchMode = 0
+ // ConstraintMatchRegExp uses regexp to match a constraint
+ ConstraintMatchRegExp ConstraintMatchMode = 1
+ // ConstraintMatchNone does not enforce a constraint
+ ConstraintMatchNone ConstraintMatchMode = 2
+)
+
+// VersionConstraint defines a constraint for comparing versions
+type VersionConstraint struct {
+ Constraint string
+ MatchFunc MatchFuncFn
+ MatchArgs interface{}
+ IgnoreList []string
+ Strategy UpdateStrategy
+ Options *options.ManifestOptions
+}
+
+type MatchFuncFn func(tagName string, pattern interface{}) bool
+
+// String returns the string representation of VersionConstraint
+func (vc *VersionConstraint) String() string {
+ return vc.Constraint
+}
+
+func NewVersionConstraint() *VersionConstraint {
+ return &VersionConstraint{
+ MatchFunc: MatchFuncNone,
+ Strategy: StrategySemVer,
+ Options: options.NewManifestOptions(),
+ }
+}
+
+// GetNewestVersionFromTags returns the latest available version from a list of
+// tags while optionally taking a semver constraint into account. Returns the
+// original version if no new version could be found from the list of tags.
+func (img *ContainerImage) GetNewestVersionFromTags(vc *VersionConstraint, tagList *tag.ImageTagList) (*tag.ImageTag, error) {
+ logCtx := log.NewContext()
+ logCtx.AddField("image", img.String())
+
+ var availableTags tag.SortableImageTagList
+ switch vc.Strategy {
+ case StrategySemVer:
+ availableTags = tagList.SortBySemVer()
+ case StrategyAlphabetical:
+ availableTags = tagList.SortAlphabetically()
+ case StrategyNewestBuild:
+ availableTags = tagList.SortByDate()
+ case StrategyDigest:
+ availableTags = tagList.SortAlphabetically()
+ }
+
+ considerTags := tag.SortableImageTagList{}
+
+ // It makes no sense to proceed if we have no available tags
+ if len(availableTags) == 0 {
+ return img.ImageTag, nil
+ }
+
+ // The given constraint MUST match a semver constraint
+ var semverConstraint *semver.Constraints
+ var err error
+ if vc.Strategy == StrategySemVer {
+ // TODO: Shall we really ensure a valid semver on the current tag?
+ // This prevents updating from a non-semver tag currently.
+ if img.ImageTag != nil && img.ImageTag.TagName != "" {
+ _, err := semver.NewVersion(img.ImageTag.TagName)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if vc.Constraint != "" {
+ if vc.Strategy == StrategySemVer {
+ semverConstraint, err = semver.NewConstraint(vc.Constraint)
+ if err != nil {
+ logCtx.Errorf("invalid constraint '%s' given: '%v'", vc, err)
+ return nil, err
+ }
+ }
+ }
+ }
+
+ // Loop through all tags to check whether it's an update candidate.
+ for _, tag := range availableTags {
+ logCtx.Tracef("Finding out whether to consider %s for being updateable", tag.TagName)
+
+ if vc.Strategy == StrategySemVer {
+ // Non-parseable tag does not mean error - just skip it
+ ver, err := semver.NewVersion(tag.TagName)
+ if err != nil {
+ logCtx.Tracef("Not a valid version: %s", tag.TagName)
+ continue
+ }
+
+ // If we have a version constraint, check image tag against it. If the
+ // constraint is not satisfied, skip tag.
+ if semverConstraint != nil {
+ if !semverConstraint.Check(ver) {
+ logCtx.Tracef("%s did not match constraint %s", ver.Original(), vc.Constraint)
+ continue
+ }
+ }
+ } else if vc.Strategy == StrategyDigest {
+ if tag.TagName != vc.Constraint {
+ logCtx.Tracef("%s did not match contraint %s", tag.TagName, vc.Constraint)
+ continue
+ }
+ }
+
+ // Append tag as update candidate
+ considerTags = append(considerTags, tag)
+ }
+
+ logCtx.Debugf("found %d from %d tags eligible for consideration", len(considerTags), len(availableTags))
+
+ // If we found tags to consider, return the most recent tag found according
+ // to the update strategy.
+ if len(considerTags) > 0 {
+ return considerTags[len(considerTags)-1], nil
+ }
+
+ return nil, nil
+}
+
+// IsTagIgnored matches tag against the patterns in IgnoreList and returns true if one of them matches
+func (vc *VersionConstraint) IsTagIgnored(tag string) bool {
+ for _, t := range vc.IgnoreList {
+ if match, err := filepath.Match(t, tag); err == nil && match {
+ log.Tracef("tag %s is ignored by pattern %s", tag, t)
+ return true
+ }
+ }
+ return false
+}
+
+// IsCacheable returns true if we can safely cache tags for strategy s
+func (s UpdateStrategy) IsCacheable() bool {
+ switch s {
+ case StrategyDigest:
+ return false
+ default:
+ return true
+ }
+}
+
+// NeedsMetadata returns true if strategy s requires image metadata to work correctly
+func (s UpdateStrategy) NeedsMetadata() bool {
+ switch s {
+ case StrategyNewestBuild:
+ return true
+ default:
+ return false
+ }
+}
+
+// NeedsVersionConstraint returns true if strategy s requires a version constraint to be defined
+func (s UpdateStrategy) NeedsVersionConstraint() bool {
+ switch s {
+ case StrategyDigest:
+ return true
+ default:
+ return false
+ }
+}
+
+// WantsOnlyConstraintTag returns true if strategy s only wants to inspect the tag specified by the constraint
+func (s UpdateStrategy) WantsOnlyConstraintTag() bool {
+ switch s {
+ case StrategyDigest:
+ return true
+ default:
+ return false
+ }
+}
diff --git a/registry-scanner/pkg/image/version_test.go b/registry-scanner/pkg/image/version_test.go
new file mode 100644
index 0000000..4c1fe2c
--- /dev/null
+++ b/registry-scanner/pkg/image/version_test.go
@@ -0,0 +1,196 @@
+package image
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag"
+)
+
+func newImageTagList(tagNames []string) *tag.ImageTagList {
+ tagList := tag.NewImageTagList()
+ for _, tagName := range tagNames {
+ tagList.Add(tag.NewImageTag(tagName, time.Unix(0, 0), ""))
+ }
+ return tagList
+}
+
+func newImageTagListWithDate(tagNames []string) *tag.ImageTagList {
+ tagList := tag.NewImageTagList()
+ for i, t := range tagNames {
+ tagList.Add(tag.NewImageTag(t, time.Unix(int64(i*5), 0), ""))
+ }
+ return tagList
+}
+
+func Test_LatestVersion(t *testing.T) {
+ t.Run("Find the latest version without any constraint", func(t *testing.T) {
+ tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"})
+ img := NewFromIdentifier("jannfis/test:1.0")
+ vc := VersionConstraint{}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ require.NotNil(t, newTag)
+ assert.Equal(t, "2.0.3", newTag.TagName)
+ })
+
+ t.Run("Find the latest version with a semver constraint on major", func(t *testing.T) {
+ tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"})
+ img := NewFromIdentifier("jannfis/test:1.0")
+ vc := VersionConstraint{Constraint: "^1.0"}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ require.NotNil(t, newTag)
+ assert.Equal(t, "1.1.2", newTag.TagName)
+ })
+
+ t.Run("Find the latest version with a semver constraint on patch", func(t *testing.T) {
+ tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"})
+ img := NewFromIdentifier("jannfis/test:1.0")
+ vc := VersionConstraint{Constraint: "~1.0"}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ require.NotNil(t, newTag)
+ assert.Equal(t, "1.0.1", newTag.TagName)
+ })
+
+ t.Run("Find the latest version with a semver constraint that has no match", func(t *testing.T) {
+ tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "2.0.3"})
+ img := NewFromIdentifier("jannfis/test:1.0")
+ vc := VersionConstraint{Constraint: "~1.0"}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ require.Nil(t, newTag)
+ })
+
+ t.Run("Find the latest version with a semver constraint that is invalid", func(t *testing.T) {
+ tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "2.0.3"})
+ img := NewFromIdentifier("jannfis/test:1.0")
+ vc := VersionConstraint{Constraint: "latest"}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ assert.Error(t, err)
+ assert.Nil(t, newTag)
+ })
+
+ t.Run("Find the latest version with no tags", func(t *testing.T) {
+ tagList := newImageTagList([]string{})
+ img := NewFromIdentifier("jannfis/test:1.0")
+ vc := VersionConstraint{Constraint: "~1.0"}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ require.NotNil(t, newTag)
+ assert.Equal(t, "1.0", newTag.TagName)
+ })
+
+ t.Run("Find the latest version using latest sortmode", func(t *testing.T) {
+ tagList := newImageTagListWithDate([]string{"zz", "bb", "yy", "cc", "yy", "aa", "ll"})
+ img := NewFromIdentifier("jannfis/test:bb")
+ vc := VersionConstraint{Strategy: StrategyNewestBuild}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ require.NotNil(t, newTag)
+ assert.Equal(t, "ll", newTag.TagName)
+ })
+
+ t.Run("Find the latest version using latest sortmode, invalid tags", func(t *testing.T) {
+ tagList := newImageTagListWithDate([]string{"zz", "bb", "yy", "cc", "yy", "aa", "ll"})
+ img := NewFromIdentifier("jannfis/test:bb")
+ vc := VersionConstraint{Strategy: StrategySemVer}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ require.NotNil(t, newTag)
+ assert.Equal(t, "bb", newTag.TagName)
+ })
+
+ t.Run("Find the latest version using VersionConstraint StrategyAlphabetical", func(t *testing.T) {
+ tagList := newImageTagListWithDate([]string{"zz", "bb", "yy", "cc", "yy", "aa", "ll"})
+ img := NewFromIdentifier("jannfis/test:bb")
+ vc := VersionConstraint{Strategy: StrategyAlphabetical}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ require.NotNil(t, newTag)
+ assert.Equal(t, "zz", newTag.TagName)
+ })
+
+ t.Run("Find the latest version using VersionConstraint StrategyDigest", func(t *testing.T) {
+ tagList := tag.NewImageTagList()
+ newDigest := "latest@sha:abcdefg"
+ tagList.Add(tag.NewImageTag("latest", time.Unix(int64(6), 0), newDigest))
+ img := NewFromIdentifier("jannfis/test:latest@sha:1234567")
+ vc := VersionConstraint{Strategy: StrategyDigest, Constraint: "latest"}
+ newTag, err := img.GetNewestVersionFromTags(&vc, tagList)
+ require.NoError(t, err)
+ assert.Equal(t, "latest", newTag.TagName)
+ assert.Equal(t, newDigest, newTag.TagDigest)
+ })
+
+}
+
+func Test_UpdateStrategy_String(t *testing.T) {
+ tests := []struct {
+ name string
+ us UpdateStrategy
+ want string
+ }{
+ {"StrategySemVer", StrategySemVer, "semver"},
+ {"StrategyNewestBuild", StrategyNewestBuild, "newest-build"},
+ {"StrategyAlphabetical", StrategyAlphabetical, "alphabetical"},
+ {"StrategyDigest", StrategyDigest, "digest"},
+ {"unknown", UpdateStrategy(-1), "unknown"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.want, tt.us.String())
+ })
+ }
+}
+
+func Test_NewVersionConstraint(t *testing.T) {
+ constraint := NewVersionConstraint()
+ assert.Equal(t, StrategySemVer, constraint.Strategy)
+ assert.Equal(t, options.NewManifestOptions(), constraint.Options)
+ assert.False(t, constraint.MatchFunc("", ""))
+}
+
+func Test_VersionConstraint_IsTagIgnored(t *testing.T) {
+ versionConstraint := VersionConstraint{IgnoreList: []string{"tag1", "tag2"}}
+ assert.True(t, versionConstraint.IsTagIgnored("tag1"))
+ assert.True(t, versionConstraint.IsTagIgnored("tag2"))
+ assert.False(t, versionConstraint.IsTagIgnored("tag3"))
+ versionConstraint.IgnoreList = []string{"tag?", "foo"}
+ assert.True(t, versionConstraint.IsTagIgnored("tag1"))
+ assert.True(t, versionConstraint.IsTagIgnored("foo"))
+ assert.False(t, versionConstraint.IsTagIgnored("tag10"))
+}
+
+func Test_UpdateStrategy_IsCacheable(t *testing.T) {
+ assert.True(t, StrategySemVer.IsCacheable())
+ assert.True(t, StrategyNewestBuild.IsCacheable())
+ assert.True(t, StrategyAlphabetical.IsCacheable())
+ assert.False(t, StrategyDigest.IsCacheable())
+}
+
+func Test_UpdateStrategy_NeedsMetadata(t *testing.T) {
+ assert.False(t, StrategySemVer.NeedsMetadata())
+ assert.True(t, StrategyNewestBuild.NeedsMetadata())
+ assert.False(t, StrategyAlphabetical.NeedsMetadata())
+ assert.False(t, StrategyDigest.NeedsMetadata())
+}
+
+func Test_UpdateStrategy_NeedsVersionConstraint(t *testing.T) {
+ assert.False(t, StrategySemVer.NeedsVersionConstraint())
+ assert.False(t, StrategyNewestBuild.NeedsVersionConstraint())
+ assert.False(t, StrategyAlphabetical.NeedsVersionConstraint())
+ assert.True(t, StrategyDigest.NeedsVersionConstraint())
+}
+
+func Test_UpdateStrategy_WantsOnlyConstraintTag(t *testing.T) {
+ assert.False(t, StrategySemVer.WantsOnlyConstraintTag())
+ assert.False(t, StrategyNewestBuild.WantsOnlyConstraintTag())
+ assert.False(t, StrategyAlphabetical.WantsOnlyConstraintTag())
+ assert.True(t, StrategyDigest.WantsOnlyConstraintTag())
+}
diff --git a/registry-scanner/pkg/kube/kubernetes.go b/registry-scanner/pkg/kube/kubernetes.go
index 6771440..0ac0946 100644
--- a/registry-scanner/pkg/kube/kubernetes.go
+++ b/registry-scanner/pkg/kube/kubernetes.go
@@ -54,12 +54,7 @@ func NewKubernetesClientFromConfig(ctx context.Context, namespace string, kubeco
return nil, err
}
- applicationsClientset, err := versioned.NewForConfig(config)
- if err != nil {
- return nil, err
- }
-
- return NewKubernetesClient(ctx, clientset, applicationsClientset, namespace), nil
+ return NewKubernetesClient(ctx, clientset, namespace), nil
}
// GetSecretData returns the raw data from named K8s secret in given namespace
diff --git a/registry-scanner/pkg/registry/registry_test.go b/registry-scanner/pkg/registry/registry_test.go
index ff525ce..eccc2b6 100644
--- a/registry-scanner/pkg/registry/registry_test.go
+++ b/registry-scanner/pkg/registry/registry_test.go
@@ -5,109 +5,115 @@ import (
"testing"
"time"
- //nolint:staticcheck
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/image"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/registry/mocks"
+ "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag"
+
+ "github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// Test relies on image package which is not available yet. Will uncomment as soon as it is available.
-// func Test_GetTags(t *testing.T) {
-
-// t.Run("Check for correctly returned tags with semver sort", func(t *testing.T) {
-// regClient := mocks.RegistryClient{}
-// regClient.On("NewRepository", mock.Anything).Return(nil)
-// regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
-
-// ep, err := GetRegistryEndpoint("")
-// require.NoError(t, err)
-
-// img := image.NewFromIdentifier("foo/bar:1.2.0")
-
-// tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{Strategy: image.StrategySemVer, Options: options.NewManifestOptions()})
-// require.NoError(t, err)
-// assert.NotEmpty(t, tl)
-
-// tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
-// require.NoError(t, err)
-// assert.Nil(t, tag)
-// })
-
-// t.Run("Check for correctly returned tags with filter function applied", func(t *testing.T) {
-// regClient := mocks.RegistryClient{}
-// regClient.On("NewRepository", mock.Anything).Return(nil)
-// regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
-
-// ep, err := GetRegistryEndpoint("")
-// require.NoError(t, err)
-
-// img := image.NewFromIdentifier("foo/bar:1.2.0")
-
-// tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{
-// Strategy: image.StrategySemVer,
-// MatchFunc: image.MatchFuncNone,
-// Options: options.NewManifestOptions()})
-// require.NoError(t, err)
-// assert.Empty(t, tl.Tags())
-
-// tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
-// require.NoError(t, err)
-// assert.Nil(t, tag)
-// })
-
-// t.Run("Check for correctly returned tags with name sort", func(t *testing.T) {
-
-// regClient := mocks.RegistryClient{}
-// regClient.On("NewRepository", mock.Anything).Return(nil)
-// regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
-
-// ep, err := GetRegistryEndpoint("")
-// require.NoError(t, err)
-
-// img := image.NewFromIdentifier("foo/bar:1.2.0")
-
-// tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{Strategy: image.StrategyAlphabetical, Options: options.NewManifestOptions()})
-// require.NoError(t, err)
-// assert.NotEmpty(t, tl)
-
-// tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
-// require.NoError(t, err)
-// assert.Nil(t, tag)
-// })
-
-// t.Run("Check for correctly returned tags with latest sort", func(t *testing.T) {
-// ts := "2006-01-02T15:04:05.999999999Z"
-// meta1 := &schema1.SignedManifest{ //nolint:staticcheck
-// Manifest: schema1.Manifest{ //nolint:staticcheck
-// History: []schema1.History{ //nolint:staticcheck
-// {
-// V1Compatibility: `{"created":"` + ts + `"}`,
-// },
-// },
-// },
-// }
-
-// regClient := mocks.RegistryClient{}
-// regClient.On("NewRepository", mock.Anything).Return(nil)
-// regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
-// regClient.On("ManifestForTag", mock.Anything, mock.Anything).Return(meta1, nil)
-// regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(&tag.TagInfo{}, nil)
-
-// ep, err := GetRegistryEndpoint("")
-// require.NoError(t, err)
-// ep.Cache.ClearCache()
-
-// img := image.NewFromIdentifier("foo/bar:1.2.0")
-// tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{Strategy: image.StrategyNewestBuild, Options: options.NewManifestOptions()})
-// require.NoError(t, err)
-// assert.NotEmpty(t, tl)
-
-// tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
-// require.NoError(t, err)
-// require.NotNil(t, tag)
-// require.Equal(t, "1.2.1", tag.TagName)
-// })
-
-// }
+func Test_GetTags(t *testing.T) {
+
+ t.Run("Check for correctly returned tags with semver sort", func(t *testing.T) {
+ regClient := mocks.RegistryClient{}
+ regClient.On("NewRepository", mock.Anything).Return(nil)
+ regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
+
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+
+ img := image.NewFromIdentifier("foo/bar:1.2.0")
+
+ tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{Strategy: image.StrategySemVer, Options: options.NewManifestOptions()})
+ require.NoError(t, err)
+ assert.NotEmpty(t, tl)
+
+ tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
+ require.NoError(t, err)
+ assert.Nil(t, tag)
+ })
+
+ t.Run("Check for correctly returned tags with filter function applied", func(t *testing.T) {
+ regClient := mocks.RegistryClient{}
+ regClient.On("NewRepository", mock.Anything).Return(nil)
+ regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
+
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+
+ img := image.NewFromIdentifier("foo/bar:1.2.0")
+
+ tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{
+ Strategy: image.StrategySemVer,
+ MatchFunc: image.MatchFuncNone,
+ Options: options.NewManifestOptions()})
+ require.NoError(t, err)
+ assert.Empty(t, tl.Tags())
+
+ tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
+ require.NoError(t, err)
+ assert.Nil(t, tag)
+ })
+
+ t.Run("Check for correctly returned tags with name sort", func(t *testing.T) {
+
+ regClient := mocks.RegistryClient{}
+ regClient.On("NewRepository", mock.Anything).Return(nil)
+ regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
+
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+
+ img := image.NewFromIdentifier("foo/bar:1.2.0")
+
+ tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{Strategy: image.StrategyAlphabetical, Options: options.NewManifestOptions()})
+ require.NoError(t, err)
+ assert.NotEmpty(t, tl)
+
+ tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
+ require.NoError(t, err)
+ assert.Nil(t, tag)
+ })
+
+ t.Run("Check for correctly returned tags with latest sort", func(t *testing.T) {
+ ts := "2006-01-02T15:04:05.999999999Z"
+ meta1 := &schema1.SignedManifest{ //nolint:staticcheck
+ Manifest: schema1.Manifest{ //nolint:staticcheck
+ History: []schema1.History{ //nolint:staticcheck
+ {
+ V1Compatibility: `{"created":"` + ts + `"}`,
+ },
+ },
+ },
+ }
+
+ regClient := mocks.RegistryClient{}
+ regClient.On("NewRepository", mock.Anything).Return(nil)
+ regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil)
+ regClient.On("ManifestForTag", mock.Anything, mock.Anything).Return(meta1, nil)
+ regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(&tag.TagInfo{}, nil)
+
+ ep, err := GetRegistryEndpoint("")
+ require.NoError(t, err)
+ ep.Cache.ClearCache()
+
+ img := image.NewFromIdentifier("foo/bar:1.2.0")
+ tl, err := ep.GetTags(img, &regClient, &image.VersionConstraint{Strategy: image.StrategyNewestBuild, Options: options.NewManifestOptions()})
+ require.NoError(t, err)
+ assert.NotEmpty(t, tl)
+
+ tag, err := ep.Cache.GetTag("foo/bar", "1.2.1")
+ require.NoError(t, err)
+ require.NotNil(t, tag)
+ require.Equal(t, "1.2.1", tag.TagName)
+ })
+
+}
func Test_ExpireCredentials(t *testing.T) {
epYAML := `
diff --git a/registry-scanner/test/fixture/fileutil.go b/registry-scanner/test/fixture/fileutil.go
new file mode 100644
index 0000000..7820b72
--- /dev/null
+++ b/registry-scanner/test/fixture/fileutil.go
@@ -0,0 +1,14 @@
+package fixture
+
+import "os"
+
+// Fixture functions for tests related to files
+
+// MustReadFile must read a file from given path. Panics if it can't.
+func MustReadFile(path string) string {
+ retBytes, err := os.ReadFile(path)
+ if err != nil {
+ panic(err)
+ }
+ return string(retBytes)
+}
diff --git a/registry-scanner/test/fixture/kubernetes.go b/registry-scanner/test/fixture/kubernetes.go
new file mode 100644
index 0000000..d9afa81
--- /dev/null
+++ b/registry-scanner/test/fixture/kubernetes.go
@@ -0,0 +1,63 @@
+package fixture
+
+import (
+ "encoding/json"
+
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// func AddPartOfArgoCDLabel(objs ...metav1.Object) {
+// for _, obj := range objs {
+// labels := obj.GetLabels()
+// if labels == nil {
+// labels = map[string]string{}
+// }
+// labels["app.kubernetes.io/part-of"] = "argocd"
+// obj.SetLabels(labels)
+// }
+// }
+
+// NewSecret creates a new Kubernetes secret object in given namespace, with
+// given name and with given data.
+func NewSecret(namespace, name string, entries map[string][]byte) *v1.Secret {
+ secret := v1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace,
+ Name: name,
+ },
+ Data: entries,
+ }
+ return &secret
+}
+
+// NewSecret creates a new Kubernetes secret object in given namespace, with
+// given name and with given data.
+func NewConfigMap(namespace, name string, entries map[string]string) *v1.ConfigMap {
+ configMap := v1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: namespace,
+ Name: name,
+ },
+ Data: entries,
+ }
+ return &configMap
+}
+
+// MustCreateSecretFromFile reads a Kubernetes secret definition from filepath
+// and returns a Secret object. Panics on error.
+func MustCreateSecretFromFile(filepath string) *v1.Secret {
+ jsonData := MustReadFile(filepath)
+ return MustCreateSecretFromJson(jsonData)
+}
+
+// MustCreateSecretFromJson creates a Kubernetes secret from given JSON data
+// and returns a Secret object. Panics on error.
+func MustCreateSecretFromJson(jsonData string) *v1.Secret {
+ var s v1.Secret
+ err := json.Unmarshal([]byte(jsonData), &s)
+ if err != nil {
+ panic(err)
+ }
+ return &s
+}
diff --git a/registry-scanner/test/testdata/docker/invalid1-config.json b/registry-scanner/test/testdata/docker/invalid1-config.json
new file mode 100644
index 0000000..244ff09
--- /dev/null
+++ b/registry-scanner/test/testdata/docker/invalid1-config.json
@@ -0,0 +1,7 @@
+{
+ "auths": {
+ "https://registry-1.docker.io/v2/": {
+ "auth": "Zm9vOmJhcg=="
+ }
+ }
+}
diff --git a/registry-scanner/test/testdata/docker/valid-config-noproto.json b/registry-scanner/test/testdata/docker/valid-config-noproto.json
new file mode 100644
index 0000000..6f72d8a
--- /dev/null
+++ b/registry-scanner/test/testdata/docker/valid-config-noproto.json
@@ -0,0 +1,7 @@
+{
+ "auths": {
+ "registry-1.docker.io": {
+ "auth": "Zm9vOmJhcg=="
+ }
+ }
+}
diff --git a/registry-scanner/test/testdata/docker/valid-config.json b/registry-scanner/test/testdata/docker/valid-config.json
new file mode 100644
index 0000000..244ff09
--- /dev/null
+++ b/registry-scanner/test/testdata/docker/valid-config.json
@@ -0,0 +1,7 @@
+{
+ "auths": {
+ "https://registry-1.docker.io/v2/": {
+ "auth": "Zm9vOmJhcg=="
+ }
+ }
+}
diff --git a/registry-scanner/test/testdata/kubernetes/config b/registry-scanner/test/testdata/kubernetes/config
new file mode 100644
index 0000000..2226e38
--- /dev/null
+++ b/registry-scanner/test/testdata/kubernetes/config
@@ -0,0 +1,19 @@
+apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURBVENDQWVtZ0F3SUJBZ0lKQU1yVlBMS3JqWGx1TUEwR0NTcUdTSWIzRFFFQkN3VUFNQmN4RlRBVEJnTlYKQkFNTURERXdMakUxTWk0eE9ETXVNVEFlRncweU1EQTFNalF4TURBeU1qbGFGdzB6TURBMU1qSXhNREF5TWpsYQpNQmN4RlRBVEJnTlZCQU1NRERFd0xqRTFNaTR4T0RNdU1UQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQCkFEQ0NBUW9DZ2dFQkFOeXhoWVZsLzBOUm43L2F4RjRaKytBNzJTcFZmbXVCNCs3QUFlREYxZXgvSVlFaDIzR2IKSzFuS25FUTZuNlgvemMyWVIveGdiekswSzJCV1paUENKME9WS2xWMklyckdtZ0xnMnpmTVB0RDZwNytFZEN4LwpIUW10MmRRUndrdHhyMFJqRUxFeUtZVE1BdHhnOFJUYi95TUVsY25iOEVFaGthZ2lGNUJKSTdreTlzYks1UlA2CkhJMUlMS2k1cE5hNGQ1YlhYNXFLeXM2dUFhY0V0OXQ3ZVdxMGtLamE0ckR5alZIYjk5WFg3QkV1SXZtVzV1WTAKMTI2UmVIT2UzUG1STGNEcGhrYVBncFZVN3Z3dmdGeldYT24xU1c3TFRXTG80K0p5UHpic3NjdFFRRjd1cWlKSgpwOFpWRURDTUluTFRDRURjRUlUdGxhdytyaXRFbS95SytQTUNBd0VBQWFOUU1FNHdIUVlEVlIwT0JCWUVGRDZmCktRdnljWUk3SzNLeFIreWdhL3o5ZDBnek1COEdBMVVkSXdRWU1CYUFGRDZmS1F2eWNZSTdLM0t4Uit5Z2EvejkKZDBnek1Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQURBWG5PYXRQNnh4cUFxbApiYXlWUEg5SDJZQk5nN3JYd3pETnpUM2JxRWswYVg4RnJDbWU0RkhhVXl6QmlISTZOSi9wbWk0TkxwblRxa2NVCnY3M1RXUmhsZjE4dzQzUzA2ZFNmSXpSank3cUhXR2dWRnRKRTcrTXRhcllNaCsydkRwMSs1by9ic21wZk5DaDcKOEhFOGlqb3lGRDYvYnFmdGIyeU5jZmJVNmF4Wll5VmYvSlpvd2grZUtnU21iQXNaZXRLWXZmSGVEWnE2SFFsaQpjZnZqUUdMMDNMSGxtSjdQNWlBSkJyVlE5MmZLS3pOejRXQWV1aktiYlB0TlBtTnVpL3EzR0cwU3o4bGJiNXpvCnRvNitJMzAwUUJReGpVd0Uvb1AvUXkyN0dMeER2aGFFTEl4V2c1R1R3Q3NDeHBGZXUzSm0yMDNNU3pOVHlJRDYKOUZTa3Rxaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+ server: https://127.0.0.1:16443
+ name: mock-cluster
+contexts:
+- context:
+ cluster: mock-cluster
+ user: admin
+ name: mock-cluster
+current-context: mock-cluster
+kind: Config
+preferences: {}
+users:
+- name: admin
+ user:
+ password: foobar
+ username: admin
diff --git a/registry-scanner/test/testdata/registry/config/two-defaults.yaml b/registry-scanner/test/testdata/registry/config/two-defaults.yaml
new file mode 100644
index 0000000..21254cd
--- /dev/null
+++ b/registry-scanner/test/testdata/registry/config/two-defaults.yaml
@@ -0,0 +1,9 @@
+registries:
+- name: "Reg One"
+ prefix: regone.io
+ api_url: "https://regone.io"
+ default: true
+- name: "Reg Two"
+ prefix: regtwo.io
+ api_url: "https://regtwo.io"
+ default: true \ No newline at end of file
diff --git a/registry-scanner/test/testdata/resources/dummy-secret.json b/registry-scanner/test/testdata/resources/dummy-secret.json
new file mode 100644
index 0000000..7055f67
--- /dev/null
+++ b/registry-scanner/test/testdata/resources/dummy-secret.json
@@ -0,0 +1,12 @@
+{
+ "apiVersion": "v1",
+ "data": {
+ "namespace": "YXJnb2Nk"
+ },
+ "kind": "Secret",
+ "metadata": {
+ "name": "test-secret",
+ "namespace": "test-namespace"
+ }
+}
+
diff --git a/registry-scanner/test/testdata/scripts/get-credentials-invalid.sh b/registry-scanner/test/testdata/scripts/get-credentials-invalid.sh
new file mode 100755
index 0000000..a6199f3
--- /dev/null
+++ b/registry-scanner/test/testdata/scripts/get-credentials-invalid.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "some gibberish foo" \ No newline at end of file
diff --git a/registry-scanner/test/testdata/scripts/get-credentials-valid.sh b/registry-scanner/test/testdata/scripts/get-credentials-valid.sh
new file mode 100755
index 0000000..ba348ef
--- /dev/null
+++ b/registry-scanner/test/testdata/scripts/get-credentials-valid.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "username:password" \ No newline at end of file