From 3aa4a0a84e1db77bd267ec95b0529828fe69bdbd Mon Sep 17 00:00:00 2001
From: David Collom <david.collom@jetstack.io>
Date: Tue, 25 Jun 2024 13:24:42 +0100
Subject: [PATCH 1/3] Second Attempt at OS/Arch Support

---
 .../templates/clusterrole.yaml                |  1 +
 deploy/charts/version-checker/values.yaml     |  4 +-
 deploy/yaml/deploy.yaml                       | 48 +++++-----
 go.mod                                        | 60 ++++++-------
 go.sum                                        | 62 ++++++++++++-
 pkg/api/types.go                              |  5 ++
 pkg/client/ghcr/ghcr.go                       |  2 +-
 pkg/client/quay/quay.go                       |  1 -
 .../checker/architecture/architecture.go      | 74 +++++++++++++++
 pkg/controller/checker/checker.go             | 30 ++++++-
 pkg/controller/controller.go                  | 17 +++-
 pkg/controller/nodes/nodes.go                 | 56 ++++++++++++
 pkg/controller/search/search.go               |  4 +-
 pkg/controller/sync.go                        | 26 ++++--
 pkg/metrics/metrics.go                        | 65 ++++++++++----
 pkg/version/utils.go                          | 89 +++++++++++++++++++
 pkg/version/version.go                        | 73 +--------------
 17 files changed, 455 insertions(+), 162 deletions(-)
 create mode 100644 pkg/controller/checker/architecture/architecture.go
 create mode 100644 pkg/controller/nodes/nodes.go
 create mode 100644 pkg/version/utils.go

diff --git a/deploy/charts/version-checker/templates/clusterrole.yaml b/deploy/charts/version-checker/templates/clusterrole.yaml
index c9d54df7..63c1be3e 100644
--- a/deploy/charts/version-checker/templates/clusterrole.yaml
+++ b/deploy/charts/version-checker/templates/clusterrole.yaml
@@ -9,6 +9,7 @@ rules:
   - ""
   resources:
   - "pods"
+  - "nodes"
   verbs:
   - "get"
   - "list"
diff --git a/deploy/charts/version-checker/values.yaml b/deploy/charts/version-checker/values.yaml
index 132ca626..84ecb46d 100644
--- a/deploy/charts/version-checker/values.yaml
+++ b/deploy/charts/version-checker/values.yaml
@@ -108,7 +108,6 @@ selfhosted:
   #  password: bar
   #  token:
 
-
 # -- Setup version-checkers resource requests/limits
 resources:
   {}
@@ -121,7 +120,8 @@ resources:
   #
 
 # -- Set container-level security context
-securityContext: {}
+securityContext:
+  {}
   # allowPrivilegeEscalation: false
   # capabilities:
   #   drop:
diff --git a/deploy/yaml/deploy.yaml b/deploy/yaml/deploy.yaml
index aac17973..6e7d40cf 100644
--- a/deploy/yaml/deploy.yaml
+++ b/deploy/yaml/deploy.yaml
@@ -49,33 +49,33 @@ spec:
     spec:
       serviceAccountName: version-checker
       containers:
-      - image: quay.io/jetstack/version-checker:v0.7.0
-        imagePullPolicy: Always
-        ports:
-        - containerPort: 8080
-        name: version-checker
-        command: ["version-checker"]
-        livenessProbe:
-          httpGet:
-            path: /readyz
-            port: 8080
-          initialDelaySeconds: 3
-          periodSeconds: 3
-        readinessProbe:
-          httpGet:
-            path: /readyz
-            port: 8080
-          initialDelaySeconds: 3
-          periodSeconds: 3
+        - image: quay.io/jetstack/version-checker:v0.7.0
+          imagePullPolicy: Always
+          ports:
+            - containerPort: 8080
+          name: version-checker
+          command: ["version-checker"]
+          livenessProbe:
+            httpGet:
+              path: /readyz
+              port: 8080
+            initialDelaySeconds: 3
+            periodSeconds: 3
+          readinessProbe:
+            httpGet:
+              path: /readyz
+              port: 8080
+            initialDelaySeconds: 3
+            periodSeconds: 3
 ---
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: version-checker
 rules:
-- apiGroups: [""]
-  resources: ["pods"]
-  verbs: ["get", "watch", "list"]
+  - apiGroups: [""]
+    resources: ["pods", "nodes"]
+    verbs: ["get", "watch", "list"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
@@ -86,6 +86,6 @@ roleRef:
   kind: ClusterRole
   name: version-checker
 subjects:
-- kind: ServiceAccount
-  name: version-checker
-  namespace: version-checker
+  - kind: ServiceAccount
+    name: version-checker
+    namespace: version-checker
diff --git a/go.mod b/go.mod
index cece0765..41cc15e1 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ toolchain go1.22.3
 require (
 	github.com/Azure/go-autorest/autorest v0.11.29
 	github.com/Azure/go-autorest/autorest/adal v0.9.24
-	github.com/aws/aws-sdk-go-v2 v1.30.0
+	github.com/aws/aws-sdk-go-v2 v1.30.1
 	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/hashicorp/go-retryablehttp v0.7.7
 	github.com/prometheus/client_golang v1.19.1
@@ -28,12 +28,12 @@ require (
 )
 
 require (
-	github.com/aws/aws-sdk-go-v2/config v1.27.21
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.21
-	github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1
+	github.com/aws/aws-sdk-go-v2/config v1.27.24
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.24
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.30.1
 	github.com/gofri/go-github-ratelimit v1.1.0
-	github.com/google/go-containerregistry v0.19.0
-	github.com/google/go-github/v58 v58.0.0
+	github.com/google/go-containerregistry v0.20.0
+	github.com/google/go-github/v62 v62.0.0
 )
 
 require (
@@ -42,25 +42,25 @@ require (
 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 // indirect
-	github.com/aws/smithy-go v1.20.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect
+	github.com/aws/smithy-go v1.20.3 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/blang/semver/v4 v4.0.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
+	github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/docker/cli v24.0.0+incompatible // indirect
-	github.com/docker/distribution v2.8.2+incompatible // indirect
-	github.com/docker/docker v24.0.9+incompatible // indirect
-	github.com/docker/docker-credential-helpers v0.7.0 // indirect
+	github.com/docker/cli v27.0.3+incompatible // indirect
+	github.com/docker/distribution v2.8.3+incompatible // indirect
+	github.com/docker/docker v27.0.3+incompatible // indirect
+	github.com/docker/docker-credential-helpers v0.8.2 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.1 // indirect
 	github.com/evanphx/json-patch v5.9.0+incompatible // indirect
 	github.com/go-errors/errors v1.5.1 // indirect
@@ -85,7 +85,7 @@ require (
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/klauspost/compress v1.16.5 // indirect
+	github.com/klauspost/compress v1.17.9 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -95,20 +95,20 @@ require (
 	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
+	github.com/opencontainers/image-spec v1.1.0 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.54.0 // indirect
+	github.com/prometheus/common v0.55.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
-	github.com/vbatts/tar-split v0.11.3 // indirect
+	github.com/vbatts/tar-split v0.11.5 // indirect
 	github.com/xlab/treeprint v1.2.0 // indirect
-	go.starlark.net v0.0.0-20240520160348-046347dcd104 // indirect
-	golang.org/x/crypto v0.24.0 // indirect
-	golang.org/x/net v0.26.0 // indirect
+	go.starlark.net v0.0.0-20240705175910-70002002b310 // indirect
+	golang.org/x/crypto v0.25.0 // indirect
+	golang.org/x/net v0.27.0 // indirect
 	golang.org/x/sync v0.7.0 // indirect
-	golang.org/x/sys v0.21.0 // indirect
-	golang.org/x/term v0.21.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	golang.org/x/term v0.22.0 // indirect
 	golang.org/x/text v0.16.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	google.golang.org/protobuf v1.34.2 // indirect
@@ -117,7 +117,7 @@ require (
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
-	k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect
+	k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/kustomize/api v0.17.2 // indirect
 	sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect
diff --git a/go.sum b/go.sum
index 699e53f6..c0c04d1c 100644
--- a/go.sum
+++ b/go.sum
@@ -19,32 +19,58 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
 github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA=
 github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
+github.com/aws/aws-sdk-go-v2 v1.30.1 h1:4y/5Dvfrhd1MxRDD77SrfsDaj8kUkkljU7XE83NPV+o=
+github.com/aws/aws-sdk-go-v2 v1.30.1/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
 github.com/aws/aws-sdk-go-v2/config v1.27.21 h1:yPX3pjGCe2hJsetlmGNB4Mngu7UPmvWPzzWCv1+boeM=
 github.com/aws/aws-sdk-go-v2/config v1.27.21/go.mod h1:4XtlEU6DzNai8RMbjSF5MgGZtYvrhBP/aKZcRtZAVdM=
+github.com/aws/aws-sdk-go-v2/config v1.27.24 h1:NM9XicZ5o1CBU/MZaHwFtimRpWx9ohAUAqkG6AqSqPo=
+github.com/aws/aws-sdk-go-v2/config v1.27.24/go.mod h1:aXzi6QJTuQRVVusAO8/NxpdTeTyr/wRcybdDtfUwJSs=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.21 h1:pjAqgzfgFhTv5grc7xPHtXCAaMapzmwA7aU+c/SZQGw=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.21/go.mod h1:nhK6PtBlfHTUDVmBLr1dg+WHCOCK+1Fu/WQyVHPsgNQ=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.24 h1:YclAsrnb1/GTQNt2nzv+756Iw4mF8AOzcDfweWwwm/M=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.24/go.mod h1:Hld7tmnAkoBQdTMNYZGzztzKRdA4fCdn9L83LOoigac=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 h1:FR+oWPFb/8qMVYMWN98bUZAGqPvLHiyqg1wqQGfUAXY=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8/go.mod h1:EgSKcHiuuakEIxJcKGzVNWh5srVAQ3jKaSrBGRYvM48=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 h1:Aznqksmd6Rfv2HQN9cpqIV/lQRMaIpJkLLaJ1ZI76no=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9/go.mod h1:WQr3MY7AxGNxaqAtsDWn+fBxmd4XvLkzeqQ8P1VM0/w=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 h1:5SAoZ4jYpGH4721ZNoS1znQrhOfZinOhc4XuTXx/nVc=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13/go.mod h1:+rdA6ZLpaSeM7tSg/B0IEDinCIBJGmW8rKDFkYpP04g=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 h1:WIijqeaAO7TYFLbhsZmi2rgLEAtWOC1LhxCAVTJlSKw=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13/go.mod h1:i+kbfa76PQbWw/ULoWnp51EYVWH4ENln76fLQE3lXT8=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
 github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 h1:ywNLJrn/Qn4enDsz/XnKlvpnLqvJxFGQV2BltWltbis=
 github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1/go.mod h1:WadVIk+UrTvWuAsCp6BKGX4i2snurpz8mPWhJQnS7Dg=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.30.1 h1:zV3FlyuyPzfyFOXKu6mJW9JBGzdtOgpdlj3va+naOD8=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.30.1/go.mod h1:l0zC7cSb2vAH1fr8+BRlolWT9cwlKpbRC8PjW6tyyIU=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 h1:zSDPny/pVnkqABXYRicYuPf9z2bTqfH13HT3v6UheIk=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14/go.mod h1:3TTcI5JSzda1nw/pkVC9dhgLre0SNBFj2lYS4GctXKI=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 h1:I9zMeF107l0rJrpnHpjEiiTSCKYAIw8mALiXcPsGBiA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15/go.mod h1:9xWJ3Q/S6Ojusz1UIkfycgD1mGirJfLLKqq3LPT7WN8=
 github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 h1:sd0BsnAvLH8gsp2e3cbaIr+9D7T1xugueQ7V/zUAsS4=
 github.com/aws/aws-sdk-go-v2/service/sso v1.21.1/go.mod h1:lcQG/MmxydijbeTOp04hIuJwXGWPZGI3bwdFDGRTv14=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 h1:p1GahKIjyMDZtiKoIn0/jAj/TkMzfzndDv5+zi2Mhgc=
+github.com/aws/aws-sdk-go-v2/service/sso v1.22.1/go.mod h1:/vWdhoIoYA5hYoPZ6fm7Sv4d8701PiG5VKe8/pPJL60=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 h1:1uEFNNskK/I1KoZ9Q8wJxMz5V9jyBlsiaNrM7vA3YUQ=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1/go.mod h1:z0P8K+cBIsFXUr5rzo/psUeJ20XjPN0+Nn8067Nd+E4=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 h1:ORnrOK0C4WmYV/uYt3koHEWBLYsRDwk2Np+eEoyV4Z0=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2/go.mod h1:xyFHA4zGxgYkdD73VeezHt3vSKEG9EmFnGwoKlP00u4=
 github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 h1:myX5CxqXE0QMZNja6FA1/FSE3Vu1rVmeUmpJMMzeZg0=
 github.com/aws/aws-sdk-go-v2/service/sts v1.29.1/go.mod h1:N2mQiucsO0VwK9CYuS4/c2n6Smeh1v47Rz3dWCPFLdE=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 h1:+woJ607dllHJQtsnJLi52ycuqHMwlW+Wqm2Ppsfp4nQ=
+github.com/aws/aws-sdk-go-v2/service/sts v1.30.1/go.mod h1:jiNR3JqT15Dm+QWq2SRgh0x0bCNSRP2L25+CqPNpJlQ=
 github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
 github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
+github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
+github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
@@ -53,6 +79,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
 github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
+github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
+github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@@ -63,12 +91,20 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM=
 github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ=
+github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
 github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
+github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
 github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
+github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
 github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
+github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
+github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
 github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
 github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
@@ -109,8 +145,10 @@ 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/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic=
 github.com/google/go-containerregistry v0.19.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
-github.com/google/go-github/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw=
-github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4=
+github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg=
+github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
+github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
+github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -146,6 +184,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 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/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -179,6 +219,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
 github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
+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/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -192,6 +234,8 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
 github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@@ -221,6 +265,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
 github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
 github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
 github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
+github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
+github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
 github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
 github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -228,6 +274,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.starlark.net v0.0.0-20240520160348-046347dcd104 h1:3qhteRISupnJvaWshOmeqEUs2y9oc/+/ePPvDh3Eygg=
 go.starlark.net v0.0.0-20240520160348-046347dcd104/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
+go.starlark.net v0.0.0-20240705175910-70002002b310 h1:tEAOMoNmN2MqVNi0MMEWpTtPI4YNCXgxmAGtuv3mST0=
+go.starlark.net v0.0.0-20240705175910-70002002b310/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -239,6 +287,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
 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.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -254,6 +304,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
 golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
 golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -279,6 +331,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
 golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -286,6 +340,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
 golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
+golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
+golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
 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=
@@ -340,6 +396,8 @@ 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-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA=
 k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc=
+k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f h1:2sXuKesAYbRHxL3aE2PN6zX/gcJr22cjrsej+W784Tc=
+k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc=
 k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak=
 k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
diff --git a/pkg/api/types.go b/pkg/api/types.go
index b7a6cbb0..ed0aba61 100644
--- a/pkg/api/types.go
+++ b/pkg/api/types.go
@@ -58,6 +58,10 @@ type Options struct {
 	PinPatch *int64 `json:"pin-patch,omitempty"`
 
 	RegexMatcher *regexp.Regexp `json:"-"`
+
+	// Architecture and OS to search for
+	Architecture *Architecture `json:"pin-architecture,omitempty"`
+	OS           *OS           `json:"pin-os,omitempty"`
 }
 
 // ImageTag describes a container image tag.
@@ -67,6 +71,7 @@ type ImageTag struct {
 	Timestamp    time.Time    `json:"timestamp"`
 	OS           OS           `json:"os,omitempty"`
 	Architecture Architecture `json:"architecture,omitempty"`
+	Children     []ImageTag   `json:"children,omitempty"`
 }
 
 type OS string
diff --git a/pkg/client/ghcr/ghcr.go b/pkg/client/ghcr/ghcr.go
index 169a12f0..03e7f6ba 100644
--- a/pkg/client/ghcr/ghcr.go
+++ b/pkg/client/ghcr/ghcr.go
@@ -7,7 +7,7 @@ import (
 	"strings"
 
 	"github.com/gofri/go-github-ratelimit/github_ratelimit"
-	"github.com/google/go-github/v58/github"
+	"github.com/google/go-github/v62/github"
 	"github.com/jetstack/version-checker/pkg/api"
 )
 
diff --git a/pkg/client/quay/quay.go b/pkg/client/quay/quay.go
index ca6031e9..fce572bb 100644
--- a/pkg/client/quay/quay.go
+++ b/pkg/client/quay/quay.go
@@ -102,7 +102,6 @@ func (c *Client) fetchImageManifest(ctx context.Context, repo, image string, tag
 	}
 
 	// Fallback to not using multi-arch image
-
 	os, arch := util.OSArchFromTag(tag.Name)
 
 	return []api.ImageTag{
diff --git a/pkg/controller/checker/architecture/architecture.go b/pkg/controller/checker/architecture/architecture.go
new file mode 100644
index 00000000..4f025518
--- /dev/null
+++ b/pkg/controller/checker/architecture/architecture.go
@@ -0,0 +1,74 @@
+package architecture
+
+import (
+	"errors"
+	"fmt"
+	"sync"
+
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/jetstack/version-checker/pkg/api"
+	"github.com/sirupsen/logrus"
+)
+
+// NodeMetadata metadata about a particular node
+type nodeMetadata struct {
+	OS           api.OS
+	Architecture api.Architecture
+}
+
+type NodeMap struct {
+	mu    sync.RWMutex
+	nodes map[string]*nodeMetadata
+}
+
+func New() *NodeMap {
+	// might need to pass an initial map
+	return &NodeMap{
+		nodes: make(map[string]*nodeMetadata),
+	}
+}
+
+func (m *NodeMap) GetArchitecture(nodeName string) (*nodeMetadata, bool) {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	meta, ok := m.nodes[nodeName]
+	return meta, ok
+}
+
+func (m *NodeMap) Add(node *corev1.Node) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if node == nil {
+		return errors.New("passed node is nil")
+	}
+
+	arch, ok := node.Labels[corev1.LabelArchStable]
+	if !ok {
+		return fmt.Errorf("missing %q label on node %q", corev1.LabelArchStable, node.Name)
+	}
+
+	os, ok := node.Labels[corev1.LabelOSStable]
+	if !ok {
+		return fmt.Errorf("missing %q label on node %q", corev1.LabelOSStable, node.Name)
+	}
+
+	logrus.WithField("module", "architecture").
+		WithField("node", node.Name).
+		WithField("os", os).
+		WithField("arch", arch).
+		Debug("node successfully added")
+
+	m.nodes[node.Name] = &nodeMetadata{
+		OS:           api.OS(os),
+		Architecture: api.Architecture(arch),
+	}
+	return nil
+}
+
+func (m *NodeMap) Delete(nodeName string) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	delete(m.nodes, nodeName)
+}
diff --git a/pkg/controller/checker/checker.go b/pkg/controller/checker/checker.go
index 1ba6b147..47ce066d 100644
--- a/pkg/controller/checker/checker.go
+++ b/pkg/controller/checker/checker.go
@@ -2,12 +2,14 @@ package checker
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"strings"
 
 	corev1 "k8s.io/api/core/v1"
 
 	"github.com/jetstack/version-checker/pkg/api"
+	"github.com/jetstack/version-checker/pkg/controller/checker/architecture"
 	"github.com/jetstack/version-checker/pkg/controller/search"
 	"github.com/jetstack/version-checker/pkg/version/semver"
 	"github.com/sirupsen/logrus"
@@ -15,6 +17,7 @@ import (
 
 type Checker struct {
 	search search.Searcher
+	nodes  *architecture.NodeMap
 }
 
 type Result struct {
@@ -22,11 +25,18 @@ type Result struct {
 	LatestVersion  string
 	IsLatest       bool
 	ImageURL       string
+	OS             api.OS
+	Architecture   api.Architecture
 }
 
-func New(search search.Searcher) *Checker {
+const (
+	nodeMissingMetadata = "error fetching node architecture"
+)
+
+func New(search search.Searcher, nodesArchInfo *architecture.NodeMap) *Checker {
 	return &Checker{
 		search: search,
+		nodes:  nodesArchInfo,
 	}
 }
 
@@ -34,6 +44,20 @@ func New(search search.Searcher) *Checker {
 func (c *Checker) Container(ctx context.Context, log *logrus.Entry,
 	pod *corev1.Pod, container *corev1.Container, opts *api.Options) (*Result, error) {
 
+	if opts == nil {
+		// create new options if nil
+		opts = new(api.Options)
+	}
+
+	// Get information about the pod node architecture
+	arch, ok := c.nodes.GetArchitecture(pod.Spec.NodeName)
+	if ok {
+		opts.OS = &arch.OS
+		opts.Architecture = &arch.Architecture
+	} else {
+		return nil, errors.New(nodeMissingMetadata)
+	}
+
 	// If the container image SHA status is not ready yet, exit early
 	statusSHA := containerStatusImageSHA(pod, container.Name)
 	if len(statusSHA) == 0 {
@@ -90,6 +114,8 @@ func (c *Checker) Container(ctx context.Context, log *logrus.Entry,
 		LatestVersion:  latestVersion,
 		IsLatest:       isLatest,
 		ImageURL:       imageURL,
+		OS:             latestImage.OS,
+		Architecture:   latestImage.Architecture,
 	}, nil
 }
 
@@ -174,6 +200,8 @@ func (c *Checker) isLatestSHA(ctx context.Context, imageURL, currentSHA string,
 		LatestVersion:  latestVersion,
 		IsLatest:       isLatest,
 		ImageURL:       imageURL,
+		OS:             latestImage.OS,
+		Architecture:   latestImage.Architecture,
 	}, nil
 }
 
diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go
index 2f63cec4..ea840180 100644
--- a/pkg/controller/controller.go
+++ b/pkg/controller/controller.go
@@ -18,6 +18,8 @@ import (
 
 	"github.com/jetstack/version-checker/pkg/client"
 	"github.com/jetstack/version-checker/pkg/controller/checker"
+	"github.com/jetstack/version-checker/pkg/controller/checker/architecture"
+	"github.com/jetstack/version-checker/pkg/controller/nodes"
 	"github.com/jetstack/version-checker/pkg/controller/scheduler"
 	"github.com/jetstack/version-checker/pkg/controller/search"
 	"github.com/jetstack/version-checker/pkg/metrics"
@@ -38,6 +40,9 @@ type Controller struct {
 	workqueue          workqueue.RateLimitingInterface
 	scheduledWorkQueue scheduler.ScheduledWorkQueue
 
+	nodesArchitecture *architecture.NodeMap
+	nodeController    *nodes.NodeInformer
+
 	metrics *metrics.Metrics
 	checker *checker.Checker
 
@@ -56,17 +61,23 @@ func New(
 	scheduledWorkQueue := scheduler.NewScheduledWorkQueue(clock.RealClock{}, workqueue.Add)
 
 	log = log.WithField("module", "controller")
+	nodesArchitecture := architecture.New()
+
 	versionGetter := version.New(log, imageClient, cacheTimeout)
 	search := search.New(log, cacheTimeout, versionGetter)
 
+	nodeController := nodes.New(log, nodesArchitecture)
+
 	c := &Controller{
 		log:                log,
 		kubeClient:         kubeClient,
 		workqueue:          workqueue,
 		scheduledWorkQueue: scheduledWorkQueue,
 		metrics:            metrics,
-		checker:            checker.New(search),
+		checker:            checker.New(search, nodesArchitecture),
 		defaultTestAll:     defaultTestAll,
+		nodesArchitecture:  nodesArchitecture,
+		nodeController:     nodeController,
 	}
 
 	return c
@@ -78,6 +89,8 @@ func (c *Controller) Run(ctx context.Context, cacheRefreshRate time.Duration) er
 
 	sharedInformerFactory := informers.NewSharedInformerFactoryWithOptions(c.kubeClient, time.Second*30)
 	c.podLister = sharedInformerFactory.Core().V1().Pods().Lister()
+	nodeInformer := c.nodeController.Register(sharedInformerFactory)
+
 	podInformer := sharedInformerFactory.Core().V1().Pods().Informer()
 	podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
 		AddFunc: c.addObject,
@@ -92,7 +105,7 @@ func (c *Controller) Run(ctx context.Context, cacheRefreshRate time.Duration) er
 
 	c.log.Info("starting control loop")
 	sharedInformerFactory.Start(ctx.Done())
-	if !cache.WaitForCacheSync(ctx.Done(), podInformer.HasSynced) {
+	if !cache.WaitForCacheSync(ctx.Done(), nodeInformer.HasSynced, podInformer.HasSynced) {
 		return fmt.Errorf("error waiting for informer caches to sync")
 	}
 
diff --git a/pkg/controller/nodes/nodes.go b/pkg/controller/nodes/nodes.go
new file mode 100644
index 00000000..a09c346a
--- /dev/null
+++ b/pkg/controller/nodes/nodes.go
@@ -0,0 +1,56 @@
+package nodes
+
+import (
+	"github.com/jetstack/version-checker/pkg/controller/checker/architecture"
+	"github.com/sirupsen/logrus"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/informers"
+	"k8s.io/client-go/tools/cache"
+)
+
+// NodeInformer is the wrapper for the k8s Node Informer
+type NodeInformer struct {
+	log   *logrus.Entry
+	nodes *architecture.NodeMap
+}
+
+// New returns a new instance of NodeInformer
+func New(log *logrus.Entry, nodes *architecture.NodeMap) *NodeInformer {
+	return &NodeInformer{
+		log:   log.WithField("module", "controller_node"),
+		nodes: nodes,
+	}
+}
+
+// Register returns the node informer with event handler set to update node architecture map
+func (c *NodeInformer) Register(sharedInformerFactory informers.SharedInformerFactory) cache.SharedIndexInformer {
+	nodeInformer := sharedInformerFactory.Core().V1().Nodes().Informer()
+	nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc: func(obj interface{}) {
+			// Add node info and data to the map
+			err := c.nodes.Add(obj.(*corev1.Node))
+			if err != nil {
+				c.log.Errorf("error adding the node %q to architecture map: %s", obj.(*corev1.Node), err)
+				return
+			}
+			c.log.Debugf("added node %q to architecture map", obj.(*corev1.Node).Name)
+		},
+		UpdateFunc: func(old, new interface{}) {
+			// override the map
+			err := c.nodes.Add(new.(*corev1.Node))
+			if err != nil {
+				c.log.Errorf("error updating the node %q in architecture map: %s", old.(*corev1.Node), err)
+				return
+			}
+			c.log.Debugf("updated node %q in architecture map", old.(*corev1.Node).Name)
+		},
+		DeleteFunc: func(obj interface{}) {
+			// remove node from the map
+			c.nodes.Delete((obj.(*corev1.Node)).Name)
+			c.log.Debugf("removed node %q from architecture map", obj.(*corev1.Node).Name)
+		},
+	})
+
+	return nodeInformer
+}
diff --git a/pkg/controller/search/search.go b/pkg/controller/search/search.go
index 43e3086c..d4c291cd 100644
--- a/pkg/controller/search/search.go
+++ b/pkg/controller/search/search.go
@@ -79,10 +79,10 @@ func calculateHashIndex(imageURL string, opts *api.Options) (string, error) {
 		return "", fmt.Errorf("failed to marshal options: %s", err)
 	}
 
-	hash := fnv.New32()
+	hash := fnv.New64()
 	if _, err := hash.Write(append(optsJSON, []byte(imageURL)...)); err != nil {
 		return "", fmt.Errorf("failed to calculate search hash: %s", err)
 	}
 
-	return fmt.Sprintf("%d", hash.Sum32()), nil
+	return fmt.Sprintf("%d", hash.Sum64()), nil
 }
diff --git a/pkg/controller/sync.go b/pkg/controller/sync.go
index 6b5a33ae..d0e8351f 100644
--- a/pkg/controller/sync.go
+++ b/pkg/controller/sync.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/jetstack/version-checker/pkg/api"
 	"github.com/jetstack/version-checker/pkg/controller/options"
+	"github.com/jetstack/version-checker/pkg/metrics"
 	versionerrors "github.com/jetstack/version-checker/pkg/version/errors"
 )
 
@@ -86,18 +87,25 @@ func (c *Controller) checkContainer(ctx context.Context, log *logrus.Entry, pod
 	}
 
 	if result.IsLatest {
-		log.Debugf("image is latest %s:%s",
-			result.ImageURL, result.CurrentVersion)
+		log.Debugf("image is latest %s [%s/%s]:%s",
+			result.ImageURL, result.OS, result.Architecture, result.CurrentVersion)
 	} else {
-		log.Debugf("image is not latest %s: %s -> %s",
-			result.ImageURL, result.CurrentVersion, result.LatestVersion)
+		log.Debugf("image is not latest %s [%s/%s]: %s -> %s",
+			result.ImageURL, result.OS, result.Architecture, result.CurrentVersion, result.LatestVersion)
 	}
 
-	c.metrics.AddImage(pod.Namespace, pod.Name,
-		container.Name, containerType,
-		result.ImageURL, result.IsLatest,
-		result.CurrentVersion, result.LatestVersion,
-	)
+	c.metrics.AddImage(&metrics.Entry{
+		Namespace:      pod.Namespace,
+		Pod:            pod.Name,
+		Container:      container.Name,
+		ContainerType:  containerType,
+		ImageURL:       result.ImageURL,
+		IsLatest:       result.IsLatest,
+		CurrentVersion: result.CurrentVersion,
+		LatestVersion:  result.LatestVersion,
+		OS:             string(result.OS),
+		Arch:           string(result.Architecture),
+	})
 
 	return nil
 }
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 58b10e70..b32ab86a 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -34,6 +34,22 @@ type cacheItem struct {
 	image          string
 	currentVersion string
 	latestVersion  string
+	os             string
+	arch           string
+}
+
+// Entry is a struct containing a single metrics label set
+type Entry struct {
+	Namespace      string
+	Pod            string
+	Container      string
+	ContainerType  string
+	ImageURL       string
+	IsLatest       bool
+	CurrentVersion string
+	LatestVersion  string
+	OS             string
+	Arch           string
 }
 
 func New(log *logrus.Entry) *Metrics {
@@ -44,7 +60,7 @@ func New(log *logrus.Entry) *Metrics {
 			Help:      "Where the container in use is using the latest upstream registry version",
 		},
 		[]string{
-			"namespace", "pod", "container", "container_type", "image", "current_version", "latest_version",
+			"namespace", "pod", "container", "container_type", "image", "current_version", "latest_version", "architecture", "os",
 		},
 	)
 
@@ -91,27 +107,29 @@ func (m *Metrics) Run(servingAddress string) error {
 	return nil
 }
 
-func (m *Metrics) AddImage(namespace, pod, container, containerType, imageURL string, isLatest bool, currentVersion, latestVersion string) {
+func (m *Metrics) AddImage(entry *Entry) {
 	// Remove old image url/version if it exists
-	m.RemoveImage(namespace, pod, container, containerType)
+	m.RemoveImage(entry.Namespace, entry.Pod, entry.Container, entry.ContainerType)
 
 	m.mu.Lock()
 	defer m.mu.Unlock()
 
 	isLatestF := 0.0
-	if isLatest {
+	if entry.IsLatest {
 		isLatestF = 1.0
 	}
 
 	m.containerImageVersion.With(
-		m.buildLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion),
+		m.buildLabels(entry),
 	).Set(isLatestF)
 
-	index := m.latestImageIndex(namespace, pod, container, containerType)
+	index := m.latestImageIndex(entry.Namespace, entry.Pod, entry.Container, entry.ContainerType)
 	m.containerCache[index] = cacheItem{
-		image:          imageURL,
-		currentVersion: currentVersion,
-		latestVersion:  latestVersion,
+		image:          entry.ImageURL,
+		currentVersion: entry.CurrentVersion,
+		latestVersion:  entry.LatestVersion,
+		os:             entry.OS,
+		arch:           entry.Arch,
 	}
 }
 
@@ -126,7 +144,16 @@ func (m *Metrics) RemoveImage(namespace, pod, container, containerType string) {
 	}
 
 	m.containerImageVersion.Delete(
-		m.buildLabels(namespace, pod, container, containerType, item.image, item.currentVersion, item.latestVersion),
+		m.buildLabels(&Entry{
+			Namespace:      namespace,
+			Pod:            pod,
+			Container:      container,
+			ImageURL:       item.image,
+			CurrentVersion: item.currentVersion,
+			LatestVersion:  item.latestVersion,
+			OS:             item.os,
+			Arch:           item.arch,
+		}),
 	)
 	delete(m.containerCache, index)
 }
@@ -135,15 +162,17 @@ func (m *Metrics) latestImageIndex(namespace, pod, container, containerType stri
 	return strings.Join([]string{namespace, pod, container, containerType}, "")
 }
 
-func (m *Metrics) buildLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion string) prometheus.Labels {
+func (m *Metrics) buildLabels(entry *Entry) prometheus.Labels {
 	return prometheus.Labels{
-		"namespace":       namespace,
-		"pod":             pod,
-		"container_type":  containerType,
-		"container":       container,
-		"image":           imageURL,
-		"current_version": currentVersion,
-		"latest_version":  latestVersion,
+		"namespace":       entry.Namespace,
+		"pod":             entry.Pod,
+		"container":       entry.Container,
+		"container_type":  entry.ContainerType,
+		"image":           entry.ImageURL,
+		"current_version": entry.CurrentVersion,
+		"latest_version":  entry.LatestVersion,
+		"architecture":    entry.Arch,
+		"os":              entry.OS,
 	}
 }
 
diff --git a/pkg/version/utils.go b/pkg/version/utils.go
new file mode 100644
index 00000000..a5ffafb4
--- /dev/null
+++ b/pkg/version/utils.go
@@ -0,0 +1,89 @@
+package version
+
+import (
+	"github.com/jetstack/version-checker/pkg/api"
+	"github.com/jetstack/version-checker/pkg/version/semver"
+)
+
+// latestSemver will return the latest ImageTag based on the given options
+// restriction, using semver. This should not be used is UseSHA has been
+// enabled.
+// TODO: add tests..
+func latestSemver(tags []api.ImageTag, opts *api.Options) (*api.ImageTag, error) {
+	var (
+		latestImageTag *api.ImageTag
+		latestV        *semver.SemVer
+	)
+
+	for i := range tags {
+		// forcing it be the specific arch and os (defalts to true, if not set)
+		if !osArchMatch(tags[i], opts) {
+			continue
+		}
+
+		v := semver.Parse(tags[i].Tag)
+
+		// If regex enabled continue here.
+		// If we match, and is less than, update latest.
+		if opts.RegexMatcher != nil {
+			if opts.RegexMatcher.MatchString(tags[i].Tag) &&
+				(latestV == nil || latestV.LessThan(v)) {
+				latestV = v
+				latestImageTag = &tags[i]
+			}
+
+			continue
+		}
+
+		// If we have declared we wont use metadata but version has it, continue.
+		if !opts.UseMetaData && v.HasMetaData() {
+			continue
+		}
+
+		if opts.PinMajor != nil && *opts.PinMajor != v.Major() {
+			continue
+		}
+		if opts.PinMinor != nil && *opts.PinMinor != v.Minor() {
+			continue
+		}
+		if opts.PinPatch != nil && *opts.PinPatch != v.Patch() {
+			continue
+		}
+
+		// If no latest yet set
+		if latestV == nil ||
+			// If the latest set is less than
+			latestV.LessThan(v) ||
+			// If the latest is the same tag, but smaller timestamp
+			(latestV.Equal(v) && tags[i].Timestamp.After(latestImageTag.Timestamp)) {
+			latestV = v
+			latestImageTag = &tags[i]
+		}
+	}
+
+	return latestImageTag, nil
+}
+
+// latestSHA will return the latest ImageTag based on image timestamps.
+func latestSHA(tags []api.ImageTag, opts *api.Options) (*api.ImageTag, error) {
+	var latestTag *api.ImageTag
+
+	for i := range tags {
+		// forcing it be the specific arch and os (defalts to true, if not set)
+		if !osArchMatch(tags[i], opts) {
+			continue
+		}
+		if latestTag == nil || tags[i].Timestamp.After(latestTag.Timestamp) {
+			latestTag = &tags[i]
+		}
+	}
+
+	return latestTag, nil
+}
+
+func osArchMatch(tag api.ImageTag, opts *api.Options) bool {
+	if opts.OS == nil || opts.Architecture == nil {
+		return true
+	}
+	return tag.OS == *opts.OS && tag.Architecture == *opts.Architecture
+}
diff --git a/pkg/version/version.go b/pkg/version/version.go
index a78ef138..35996c9c 100644
--- a/pkg/version/version.go
+++ b/pkg/version/version.go
@@ -13,7 +13,6 @@ import (
 
 	"github.com/jetstack/version-checker/pkg/cache"
 	versionerrors "github.com/jetstack/version-checker/pkg/version/errors"
-	"github.com/jetstack/version-checker/pkg/version/semver"
 )
 
 type Version struct {
@@ -48,6 +47,7 @@ func (v *Version) LatestTagFromImage(ctx context.Context, imageURL string, opts
 		v.log.Debugf("overriding image lookup %s -> %s", imageURL, *override)
 		imageURL = *override
 	}
+
 	tagsI, err := v.imageCache.Get(ctx, imageURL, imageURL, nil)
 	if err != nil {
 		return nil, err
@@ -58,7 +58,7 @@ func (v *Version) LatestTagFromImage(ctx context.Context, imageURL string, opts
 
 	// If UseSHA then return early
 	if opts.UseSHA {
-		tag, err = latestSHA(tags)
+		tag, err = latestSHA(tags, opts)
 		if err != nil {
 			return nil, err
 		}
@@ -69,7 +69,7 @@ func (v *Version) LatestTagFromImage(ctx context.Context, imageURL string, opts
 		}
 
 	} else {
-		tag, err = latestSemver(opts, tags)
+		tag, err = latestSemver(tags, opts)
 		if err != nil {
 			return nil, err
 		}
@@ -101,70 +101,3 @@ func (v *Version) Fetch(ctx context.Context, imageURL string, _ *api.Options) (i
 
 	return tags, nil
 }
-
-// latestSemver will return the latest ImageTag based on the given options
-// restriction, using semver. This should not be used is UseSHA has been
-// enabled.
-// TODO: add tests..
-func latestSemver(opts *api.Options, tags []api.ImageTag) (*api.ImageTag, error) {
-	var (
-		latestImageTag *api.ImageTag
-		latestV        *semver.SemVer
-	)
-
-	for i := range tags {
-		v := semver.Parse(tags[i].Tag)
-
-		// If regex enabled continue here.
-		// If we match, and is less than, update latest.
-		if opts.RegexMatcher != nil {
-			if opts.RegexMatcher.MatchString(tags[i].Tag) &&
-				(latestV == nil || latestV.LessThan(v)) {
-				latestV = v
-				latestImageTag = &tags[i]
-			}
-
-			continue
-		}
-
-		// If we have declared we wont use metadata but version has it, continue.
-		if !opts.UseMetaData && v.HasMetaData() {
-			continue
-		}
-
-		if opts.PinMajor != nil && *opts.PinMajor != v.Major() {
-			continue
-		}
-		if opts.PinMinor != nil && *opts.PinMinor != v.Minor() {
-			continue
-		}
-		if opts.PinPatch != nil && *opts.PinPatch != v.Patch() {
-			continue
-		}
-
-		// If no latest yet set
-		if latestV == nil ||
-			// If the latest set is less than
-			latestV.LessThan(v) ||
-			// If the latest is the same tag, but smaller timestamp
-			(latestV.Equal(v) && tags[i].Timestamp.After(latestImageTag.Timestamp)) {
-			latestV = v
-			latestImageTag = &tags[i]
-		}
-	}
-
-	return latestImageTag, nil
-}
-
-// latestSHA will return the latest ImageTag based on image timestamps.
-func latestSHA(tags []api.ImageTag) (*api.ImageTag, error) {
-	var latestTag *api.ImageTag
-
-	for i := range tags {
-		if latestTag == nil || tags[i].Timestamp.After(latestTag.Timestamp) {
-			latestTag = &tags[i]
-		}
-	}
-
-	return latestTag, nil
-}

From dfaf915ef8129c998d27666723d9a41353fb7632 Mon Sep 17 00:00:00 2001
From: David Collom <david.collom@jetstack.io>
Date: Tue, 9 Jul 2024 10:54:12 +0100
Subject: [PATCH 2/3] Update tests

---
 cmd/app/options.go                     |  63 +++++++
 go.mod                                 |  40 ++++-
 go.sum                                 | 205 ++++++++++++++-------
 pkg/api/types.go                       |  44 +++++
 pkg/client/acr/acr.go                  | 163 +++--------------
 pkg/client/acr/auth.go                 | 153 ++++++++++++++++
 pkg/client/acr/types.go                |  58 ++++++
 pkg/client/docker/docker.go            |  41 +----
 pkg/client/docker/docker_test.go       | 226 +++++++++++++++++++++++
 pkg/client/docker/types.go             |  40 +++++
 pkg/client/ecr/ecr.go                  | 121 ++++++++++++-
 pkg/client/gcr/gar.go                  |  60 +++++++
 pkg/client/gcr/gcr.go                  |  15 +-
 pkg/client/gcr/path.go                 |   1 -
 pkg/client/gcr/types.go                |  70 ++++++++
 pkg/client/ghcr/ghcr.go                |  16 +-
 pkg/client/ghcr/ghcr_test.go           | 167 +++++++++++++++++
 pkg/client/quay/quay.go                |  46 +----
 pkg/client/quay/types.go               |  45 +++++
 pkg/client/selfhosted/path_test.go     |  10 ++
 pkg/client/selfhosted/selfhosted.go    |  69 +++----
 pkg/client/selfhosted/types.go         |  54 ++++++
 pkg/client/util/util.go                |  12 +-
 pkg/controller/checker/checker_test.go | 240 +++++++++----------------
 pkg/controller/sync.go                 |  17 +-
 pkg/metrics/metrics.go                 |  42 +++--
 pkg/metrics/metrics_test.go            | 122 ++++++++++---
 pkg/version/utils.go                   |   2 +-
 28 files changed, 1591 insertions(+), 551 deletions(-)
 create mode 100644 pkg/client/acr/auth.go
 create mode 100644 pkg/client/acr/types.go
 create mode 100644 pkg/client/docker/docker_test.go
 create mode 100644 pkg/client/docker/types.go
 create mode 100644 pkg/client/gcr/gar.go
 create mode 100644 pkg/client/gcr/types.go
 create mode 100644 pkg/client/ghcr/ghcr_test.go
 create mode 100644 pkg/client/quay/types.go
 create mode 100644 pkg/client/selfhosted/types.go

diff --git a/cmd/app/options.go b/cmd/app/options.go
index 358a04af..54fee420 100644
--- a/cmd/app/options.go
+++ b/cmd/app/options.go
@@ -25,6 +25,10 @@ const (
 	envACRPassword     = "ACR_PASSWORD"
 	envACRRefreshToken = "ACR_REFRESH_TOKEN"
 
+	envACRAppID        = "ACR_APP_ID"
+	envACRTenantID     = "ACR_TENANT_ID"
+	envACRClientSecret = "ACR_CLIENT_SECRET"
+
 	envDockerUsername = "DOCKER_USERNAME"
 	envDockerPassword = "DOCKER_PASSWORD"
 	envDockerToken    = "DOCKER_TOKEN"
@@ -47,6 +51,8 @@ const (
 	envSelfhostedBearer    = "TOKEN"
 	envSelfhostedTokenPath = "TOKEN_PATH"
 	envSelfhostedInsecure  = "INSECURE"
+	envSelfhostedTimeout   = "TIMEOUT"
+	envSelfhostedRetries   = "RETRIES"
 	envSelfhostedCAPath    = "CA_PATH"
 )
 
@@ -57,6 +63,8 @@ var (
 	selfhostedTokenPath   = regexp.MustCompile("^VERSION_CHECKER_SELFHOSTED_TOKEN_PATH_(.*)")
 	selfhostedTokenReg    = regexp.MustCompile("^VERSION_CHECKER_SELFHOSTED_TOKEN_(.*)")
 	selfhostedCAPath      = regexp.MustCompile("^VERSION_CHECKER_SELFHOSTED_CA_PATH_(.*)")
+	selfhostedTimeout     = regexp.MustCompile("^VERSION_CHECKER_SELFHOSTED_TIMEOUT_(.*)")
+	selfhostedRetryMax    = regexp.MustCompile("^VERSION_CHECKER_SELFHOSTED_RETRIES_(.*)")
 	selfhostedInsecureReg = regexp.MustCompile("^VERSION_CHECKER_SELFHOSTED_INSECURE_(.*)")
 )
 
@@ -140,6 +148,27 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
 				"username/password (%s_%s).",
 			envPrefix, envACRRefreshToken,
 		))
+	fs.StringVar(&o.Client.ACR.AppID,
+		"acr-app-id", "",
+		fmt.Sprintf(
+			"App ID to authenticate with azure container registry, to be used with "+
+				"client-secret/tenant-id (%s_%s).",
+			envPrefix, envACRAppID,
+		))
+	fs.StringVar(&o.Client.ACR.TenantID,
+		"acr-tenant-id", "",
+		fmt.Sprintf(
+			"client ID to authenticate with azure container registry, to be used with "+
+				"client-secret/app-id (%s_%s).",
+			envPrefix, envACRTenantID,
+		))
+	fs.StringVar(&o.Client.ACR.ClientSecret,
+		"acr-client-secret", "",
+		fmt.Sprintf(
+			"Client Secret to authenticate with azure container registry, to be used with "+
+				"tenant-id/app-id (%s_%s).",
+			envPrefix, envACRClientSecret,
+		))
 	///
 
 	// Docker
@@ -264,6 +293,18 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) {
 				"THIS IS NOT RECOMMENDED AND IS INTENDED FOR DEBUGGING (%s_%s)",
 			envPrefix, envSelfhostedInsecure,
 		))
+	fs.IntVarP(&o.selfhosted.Timeout,
+		"selfhosted-timeout", "", 10,
+		fmt.Sprintf(
+			"Timeout for API Calls to the Registry (%s_%s)",
+			envPrefix, envSelfhostedTimeout,
+		))
+	fs.IntVarP(&o.selfhosted.RetryMax,
+		"selfhosted-retries", "", 10,
+		fmt.Sprintf(
+			"Max number of retries per request to the Registry (%s_%s)",
+			envPrefix, envSelfhostedRetries,
+		))
 	///
 }
 
@@ -278,6 +319,9 @@ func (o *Options) complete() {
 		{envACRUsername, &o.Client.ACR.Username},
 		{envACRPassword, &o.Client.ACR.Password},
 		{envACRRefreshToken, &o.Client.ACR.RefreshToken},
+		{envACRAppID, &o.Client.ACR.AppID},
+		{envACRTenantID, &o.Client.ACR.TenantID},
+		{envACRClientSecret, &o.Client.ACR.ClientSecret},
 
 		{envDockerUsername, &o.Client.Docker.Username},
 		{envDockerPassword, &o.Client.Docker.Password},
@@ -365,6 +409,25 @@ func (o *Options) assignSelfhosted(envs []string) {
 			continue
 		}
 
+		if matches := selfhostedTimeout.FindStringSubmatch(strings.ToUpper(pair[0])); len(matches) == 2 {
+			initOptions(matches[1])
+			i, err := strconv.Atoi(pair[1])
+			if err != nil {
+				continue
+			}
+			o.Client.Selfhosted[matches[1]].Timeout = i
+			continue
+		}
+		if matches := selfhostedRetryMax.FindStringSubmatch(strings.ToUpper(pair[0])); len(matches) == 2 {
+			initOptions(matches[1])
+			i, err := strconv.Atoi(pair[1])
+			if err != nil {
+				continue
+			}
+			o.Client.Selfhosted[matches[1]].Timeout = i
+			continue
+		}
+
 		if matches := selfhostedInsecureReg.FindStringSubmatch(strings.ToUpper(pair[0])); len(matches) == 2 {
 			initOptions(matches[1])
 			val, err := strconv.ParseBool(pair[1])
diff --git a/go.mod b/go.mod
index 41cc15e1..e6e6e255 100644
--- a/go.mod
+++ b/go.mod
@@ -10,15 +10,14 @@ toolchain go1.22.3
 
 require (
 	github.com/Azure/go-autorest/autorest v0.11.29
-	github.com/Azure/go-autorest/autorest/adal v0.9.24
+	github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect
 	github.com/aws/aws-sdk-go-v2 v1.30.1
-	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/hashicorp/go-retryablehttp v0.7.7
 	github.com/prometheus/client_golang v1.19.1
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.8.1
 	github.com/spf13/pflag v1.0.5
-	golang.org/x/oauth2 v0.21.0 // indirect
+	golang.org/x/oauth2 v0.21.0
 	k8s.io/api v0.30.2
 	k8s.io/apimachinery v0.30.2
 	k8s.io/cli-runtime v0.30.2
@@ -28,20 +27,34 @@ require (
 )
 
 require (
+	cloud.google.com/go/artifactregistry v1.14.11
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
 	github.com/aws/aws-sdk-go-v2/config v1.27.24
 	github.com/aws/aws-sdk-go-v2/credentials v1.17.24
 	github.com/aws/aws-sdk-go-v2/service/ecr v1.30.1
 	github.com/gofri/go-github-ratelimit v1.1.0
 	github.com/google/go-containerregistry v0.20.0
 	github.com/google/go-github/v62 v62.0.0
+	github.com/jarcoal/httpmock v1.3.1
+	github.com/stretchr/testify v1.9.0
+	google.golang.org/api v0.187.0
 )
 
 require (
+	cloud.google.com/go v0.115.0 // indirect
+	cloud.google.com/go/auth v0.6.1 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
+	cloud.google.com/go/compute/metadata v0.4.0 // indirect
+	cloud.google.com/go/iam v1.1.10 // indirect
+	cloud.google.com/go/longrunning v0.5.9 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.1 // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 // indirect
@@ -59,25 +72,31 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/docker/cli v27.0.3+incompatible // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
-	github.com/docker/docker v27.0.3+incompatible // indirect
 	github.com/docker/docker-credential-helpers v0.8.2 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.1 // indirect
 	github.com/evanphx/json-patch v5.9.0+incompatible // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/go-errors/errors v1.5.1 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/btree v1.1.2 // indirect
 	github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.12.5 // indirect
 	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/imdario/mergo v0.3.16 // indirect
@@ -86,6 +105,7 @@ require (
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.17.9 // indirect
+	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -97,12 +117,20 @@ require (
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.1.0 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/common v0.55.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/vbatts/tar-split v0.11.5 // indirect
 	github.com/xlab/treeprint v1.2.0 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
+	go.opentelemetry.io/otel v1.28.0 // indirect
+	go.opentelemetry.io/otel/metric v1.28.0 // indirect
+	go.opentelemetry.io/otel/trace v1.28.0 // indirect
 	go.starlark.net v0.0.0-20240705175910-70002002b310 // indirect
 	golang.org/x/crypto v0.25.0 // indirect
 	golang.org/x/net v0.27.0 // indirect
@@ -111,6 +139,10 @@ require (
 	golang.org/x/term v0.22.0 // indirect
 	golang.org/x/text v0.16.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
+	google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240708141625-4ad9e859172b // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
+	google.golang.org/grpc v1.65.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
diff --git a/go.sum b/go.sum
index c0c04d1c..80b91f55 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,24 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
+cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
+cloud.google.com/go/artifactregistry v1.14.11 h1:NZzHn5lPKyi2kgtM9Atu6IBvqslL7Fu1+5EkOYZd+yk=
+cloud.google.com/go/artifactregistry v1.14.11/go.mod h1:ahyKXer42EOIddYzk2zYfvZnByGPdAYhXqBbRBsGizE=
+cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38=
+cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4=
+cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
+cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
+cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c=
+cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
+cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI=
+cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps=
+cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k=
+cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.1 h1:Xy/qV1DyOhhqsU/z0PyFMJfYCxnzna+vBEUtFW0ksQo=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.1/go.mod h1:oib6iWdC+sILvNUoJbbBn3xv7TXow7mEp/WRcsYvmow=
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
@@ -16,72 +37,48 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
-github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA=
-github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/aws/aws-sdk-go-v2 v1.30.1 h1:4y/5Dvfrhd1MxRDD77SrfsDaj8kUkkljU7XE83NPV+o=
 github.com/aws/aws-sdk-go-v2 v1.30.1/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
-github.com/aws/aws-sdk-go-v2/config v1.27.21 h1:yPX3pjGCe2hJsetlmGNB4Mngu7UPmvWPzzWCv1+boeM=
-github.com/aws/aws-sdk-go-v2/config v1.27.21/go.mod h1:4XtlEU6DzNai8RMbjSF5MgGZtYvrhBP/aKZcRtZAVdM=
 github.com/aws/aws-sdk-go-v2/config v1.27.24 h1:NM9XicZ5o1CBU/MZaHwFtimRpWx9ohAUAqkG6AqSqPo=
 github.com/aws/aws-sdk-go-v2/config v1.27.24/go.mod h1:aXzi6QJTuQRVVusAO8/NxpdTeTyr/wRcybdDtfUwJSs=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.21 h1:pjAqgzfgFhTv5grc7xPHtXCAaMapzmwA7aU+c/SZQGw=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.21/go.mod h1:nhK6PtBlfHTUDVmBLr1dg+WHCOCK+1Fu/WQyVHPsgNQ=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.24 h1:YclAsrnb1/GTQNt2nzv+756Iw4mF8AOzcDfweWwwm/M=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.24/go.mod h1:Hld7tmnAkoBQdTMNYZGzztzKRdA4fCdn9L83LOoigac=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 h1:FR+oWPFb/8qMVYMWN98bUZAGqPvLHiyqg1wqQGfUAXY=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8/go.mod h1:EgSKcHiuuakEIxJcKGzVNWh5srVAQ3jKaSrBGRYvM48=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 h1:Aznqksmd6Rfv2HQN9cpqIV/lQRMaIpJkLLaJ1ZI76no=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9/go.mod h1:WQr3MY7AxGNxaqAtsDWn+fBxmd4XvLkzeqQ8P1VM0/w=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 h1:5SAoZ4jYpGH4721ZNoS1znQrhOfZinOhc4XuTXx/nVc=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13/go.mod h1:+rdA6ZLpaSeM7tSg/B0IEDinCIBJGmW8rKDFkYpP04g=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 h1:WIijqeaAO7TYFLbhsZmi2rgLEAtWOC1LhxCAVTJlSKw=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13/go.mod h1:i+kbfa76PQbWw/ULoWnp51EYVWH4ENln76fLQE3lXT8=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 h1:ywNLJrn/Qn4enDsz/XnKlvpnLqvJxFGQV2BltWltbis=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1/go.mod h1:WadVIk+UrTvWuAsCp6BKGX4i2snurpz8mPWhJQnS7Dg=
 github.com/aws/aws-sdk-go-v2/service/ecr v1.30.1 h1:zV3FlyuyPzfyFOXKu6mJW9JBGzdtOgpdlj3va+naOD8=
 github.com/aws/aws-sdk-go-v2/service/ecr v1.30.1/go.mod h1:l0zC7cSb2vAH1fr8+BRlolWT9cwlKpbRC8PjW6tyyIU=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 h1:zSDPny/pVnkqABXYRicYuPf9z2bTqfH13HT3v6UheIk=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14/go.mod h1:3TTcI5JSzda1nw/pkVC9dhgLre0SNBFj2lYS4GctXKI=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 h1:I9zMeF107l0rJrpnHpjEiiTSCKYAIw8mALiXcPsGBiA=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15/go.mod h1:9xWJ3Q/S6Ojusz1UIkfycgD1mGirJfLLKqq3LPT7WN8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 h1:sd0BsnAvLH8gsp2e3cbaIr+9D7T1xugueQ7V/zUAsS4=
-github.com/aws/aws-sdk-go-v2/service/sso v1.21.1/go.mod h1:lcQG/MmxydijbeTOp04hIuJwXGWPZGI3bwdFDGRTv14=
 github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 h1:p1GahKIjyMDZtiKoIn0/jAj/TkMzfzndDv5+zi2Mhgc=
 github.com/aws/aws-sdk-go-v2/service/sso v1.22.1/go.mod h1:/vWdhoIoYA5hYoPZ6fm7Sv4d8701PiG5VKe8/pPJL60=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 h1:1uEFNNskK/I1KoZ9Q8wJxMz5V9jyBlsiaNrM7vA3YUQ=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1/go.mod h1:z0P8K+cBIsFXUr5rzo/psUeJ20XjPN0+Nn8067Nd+E4=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 h1:ORnrOK0C4WmYV/uYt3koHEWBLYsRDwk2Np+eEoyV4Z0=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2/go.mod h1:xyFHA4zGxgYkdD73VeezHt3vSKEG9EmFnGwoKlP00u4=
-github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 h1:myX5CxqXE0QMZNja6FA1/FSE3Vu1rVmeUmpJMMzeZg0=
-github.com/aws/aws-sdk-go-v2/service/sts v1.29.1/go.mod h1:N2mQiucsO0VwK9CYuS4/c2n6Smeh1v47Rz3dWCPFLdE=
 github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 h1:+woJ607dllHJQtsnJLi52ycuqHMwlW+Wqm2Ppsfp4nQ=
 github.com/aws/aws-sdk-go-v2/service/sts v1.30.1/go.mod h1:jiNR3JqT15Dm+QWq2SRgh0x0bCNSRP2L25+CqPNpJlQ=
-github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
-github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
 github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
 github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/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/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
-github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
+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/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
 github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -89,32 +86,31 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 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/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM=
-github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ=
 github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
-github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
-github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
-github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
-github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
 github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
 github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
 github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
 github.com/emicklei/go-restful/v3 v3.12.1/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/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
 github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
 github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 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-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
 github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@@ -133,18 +129,36 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU=
 github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M=
+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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 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/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic=
-github.com/google/go-containerregistry v0.19.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
 github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg=
 github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
 github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
@@ -156,10 +170,17 @@ 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-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
 github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.1.2/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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
+github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -172,6 +193,8 @@ 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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
+github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -182,14 +205,14 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
-github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
 github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -198,6 +221,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
+github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
@@ -217,12 +242,12 @@ 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-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
-github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
 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/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 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=
@@ -230,20 +255,18 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 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-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
-github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
 github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
 github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
-github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/sirupsen/logrus v1.9.0/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.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -252,8 +275,9 @@ 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -262,9 +286,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
-github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
-github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
 github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
 github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
 github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
@@ -272,8 +293,18 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd
 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.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-go.starlark.net v0.0.0-20240520160348-046347dcd104 h1:3qhteRISupnJvaWshOmeqEUs2y9oc/+/ePPvDh3Eygg=
-go.starlark.net v0.0.0-20240520160348-046347dcd104/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
+go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
+go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
 go.starlark.net v0.0.0-20240705175910-70002002b310 h1:tEAOMoNmN2MqVNi0MMEWpTtPI4YNCXgxmAGtuv3mST0=
 go.starlark.net v0.0.0-20240705175910-70002002b310/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -285,29 +316,37 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
-golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
 golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
 golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+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.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
-golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
 golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
 golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
 golang.org/x/oauth2 v0.21.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-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=
@@ -315,6 +354,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
 golang.org/x/sync v0.7.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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -325,12 +365,10 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
-golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
 golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -338,8 +376,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
-golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
 golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
 golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -354,6 +390,10 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 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-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -365,6 +405,35 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo=
+google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk=
+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-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b h1:dSTjko30weBaMj3eERKc0ZVXW4GudCswM3m+P++ukU0=
+google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240708141625-4ad9e859172b h1:y/kpOWeX2pWERnbsvh/hF+Zmo69wVmjyZhstreXQQeA=
+google.golang.org/genproto/googleapis/api v0.0.0-20240708141625-4ad9e859172b/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
+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.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
+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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
 google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -382,6 +451,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
 gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+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.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI=
 k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI=
 k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg=
@@ -394,8 +465,6 @@ k8s.io/component-base v0.30.2 h1:pqGBczYoW1sno8q9ObExUqrYSKhtE5rW3y6gX88GZII=
 k8s.io/component-base v0.30.2/go.mod h1:yQLkQDrkK8J6NtP+MGJOws+/PPeEXNpwFixsUI7h/OE=
 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-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA=
-k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc=
 k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f h1:2sXuKesAYbRHxL3aE2PN6zX/gcJr22cjrsej+W784Tc=
 k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc=
 k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak=
diff --git a/pkg/api/types.go b/pkg/api/types.go
index ed0aba61..fe36079d 100644
--- a/pkg/api/types.go
+++ b/pkg/api/types.go
@@ -76,3 +76,47 @@ type ImageTag struct {
 
 type OS string
 type Architecture string
+
+func (i *ImageTag) HasChildren() bool {
+	return len(i.Children) > 0
+}
+
+func (i *ImageTag) HasArchOS(arch Architecture, os OS) bool {
+	if i.matchArchOS(arch, os) {
+		return true
+	}
+
+	for _, c := range i.Children {
+		if c.matchArchOS(arch, os) {
+			return true
+		}
+	}
+
+	return true
+}
+
+func (i *ImageTag) MatchSHA(sha string) bool {
+	if i.SHA == sha {
+		return true
+	}
+
+	for _, c := range i.Children {
+		if c.MatchSHA(sha) {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (i *ImageTag) matchArchOS(arch Architecture, os OS) bool {
+	if i.OS != "" && i.OS != os {
+		return false
+	}
+
+	if i.Architecture != "" && i.Architecture != arch {
+		return false
+	}
+
+	return true
+}
diff --git a/pkg/client/acr/acr.go b/pkg/client/acr/acr.go
index 630f8989..fe84cb96 100644
--- a/pkg/client/acr/acr.go
+++ b/pkg/client/acr/acr.go
@@ -7,52 +7,14 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"sync"
 	"time"
 
 	"github.com/Azure/go-autorest/autorest"
-	"github.com/Azure/go-autorest/autorest/adal"
-	jwt "github.com/golang-jwt/jwt/v5"
 
 	"github.com/jetstack/version-checker/pkg/api"
 	"github.com/jetstack/version-checker/pkg/client/util"
 )
 
-const (
-	userAgent = "jetstack/version-checker"
-)
-
-type Client struct {
-	*http.Client
-	Options
-
-	cacheMu         sync.Mutex
-	cachedACRClient map[string]*acrClient
-}
-
-type acrClient struct {
-	tokenExpiry time.Time
-	*autorest.Client
-}
-
-type Options struct {
-	Username     string
-	Password     string
-	RefreshToken string
-}
-
-type ACRAccessTokenResponse struct {
-	AccessToken string `json:"access_token"`
-}
-
-type ACRManifestResponse struct {
-	Manifests []struct {
-		Digest      string    `json:"digest"`
-		CreatedTime time.Time `json:"createdTime"`
-		Tags        []string  `json:"tags"`
-	} `json:"manifests"`
-}
-
 func New(opts Options) (*Client, error) {
 	client := &http.Client{
 		Timeout: time.Second * 5,
@@ -75,6 +37,8 @@ func (c *Client) Name() string {
 }
 
 func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.ImageTag, error) {
+	var tags []api.ImageTag
+
 	client, err := c.getACRClient(ctx, host)
 	if err != nil {
 		return nil, err
@@ -87,26 +51,27 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.Imag
 
 	var manifestResp ACRManifestResponse
 	if err := json.NewDecoder(resp.Body).Decode(&manifestResp); err != nil {
-		return nil, fmt.Errorf("%s: failed to decode manifest response: %s",
-			host, err)
+		return nil, fmt.Errorf("%s: failed to decode manifest response: %s", host, err)
 	}
 
-	var tags []api.ImageTag
 	for _, manifest := range manifestResp.Manifests {
 		if len(manifest.Tags) == 0 {
 			tags = append(tags, api.ImageTag{
-				SHA:       manifest.Digest,
-				Timestamp: manifest.CreatedTime,
+				SHA:          manifest.Digest,
+				Timestamp:    manifest.CreatedTime,
+				OS:           manifest.OS,
+				Architecture: manifest.Architecture,
 			})
-
 			continue
 		}
 
 		for _, tag := range manifest.Tags {
 			tags = append(tags, api.ImageTag{
-				SHA:       manifest.Digest,
-				Timestamp: manifest.CreatedTime,
-				Tag:       tag,
+				SHA:          manifest.Digest,
+				Timestamp:    manifest.CreatedTime,
+				Tag:          tag,
+				OS:           manifest.OS,
+				Architecture: manifest.Architecture,
 			})
 		}
 	}
@@ -154,7 +119,7 @@ func (c *Client) getACRClient(ctx context.Context, host string) (*acrClient, err
 	c.cacheMu.Lock()
 	defer c.cacheMu.Unlock()
 
-	if client, ok := c.cachedACRClient[host]; ok && time.Now().After(client.tokenExpiry) {
+	if client, ok := c.cachedACRClient[host]; ok && time.Now().Before(client.tokenExpiry) {
 		return client, nil
 	}
 
@@ -163,10 +128,22 @@ func (c *Client) getACRClient(ctx context.Context, host string) (*acrClient, err
 		err    error
 	)
 
-	if len(c.RefreshToken) > 0 {
-		client, err = c.getAccessTokenClient(ctx, host)
+	authOpts := AuthOptions{
+		Username:     c.Options.Username,
+		Password:     c.Options.Password,
+		TenantID:     c.Options.TenantID,
+		AppID:        c.Options.AppID,
+		ClientSecret: c.Options.ClientSecret,
+		RefreshToken: c.Options.RefreshToken,
+	}
+	if len(authOpts.RefreshToken) > 0 {
+		client, err = getAccessTokenClient(ctx, authOpts, host)
+	} else if authOpts.Username != "" && authOpts.Password != "" {
+		client, err = getBasicAuthClient(authOpts, host)
+	} else if authOpts.TenantID != "" && authOpts.AppID != "" && authOpts.ClientSecret != "" {
+		client, err = getServicePrincipalClient(ctx, authOpts, host)
 	} else {
-		client, err = c.getBasicAuthClient(host)
+		client, err = getManagedIdentityClient(ctx, host)
 	}
 	if err != nil {
 		return nil, err
@@ -176,87 +153,3 @@ func (c *Client) getACRClient(ctx context.Context, host string) (*acrClient, err
 
 	return client, nil
 }
-
-func (c *Client) getBasicAuthClient(host string) (*acrClient, error) {
-	client := autorest.NewClientWithUserAgent(userAgent)
-	client.Authorizer = autorest.NewBasicAuthorizer(c.Username, c.Password)
-
-	return &acrClient{
-		Client:      &client,
-		tokenExpiry: time.Unix(1<<63-1, 0),
-	}, nil
-}
-
-func (c *Client) getAccessTokenClient(ctx context.Context, host string) (*acrClient, error) {
-	client := autorest.NewClientWithUserAgent(userAgent)
-	urlParameters := map[string]interface{}{
-		"url": "https://" + host,
-	}
-
-	formDataParameters := map[string]interface{}{
-		"grant_type":    "refresh_token",
-		"refresh_token": c.RefreshToken,
-		"scope":         "repository:*:*",
-		"service":       host,
-	}
-
-	preparer := autorest.CreatePreparer(
-		autorest.AsPost(),
-		autorest.WithCustomBaseURL("{url}", urlParameters),
-		autorest.WithPath("/oauth2/token"),
-		autorest.WithFormData(autorest.MapToValues(formDataParameters)))
-	req, err := preparer.Prepare((&http.Request{}).WithContext(ctx))
-	if err != nil {
-		return nil, err
-	}
-
-	resp, err := autorest.SendWithSender(client, req,
-		autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...))
-	if err != nil {
-		return nil, fmt.Errorf("%s: failed to request access token: %s",
-			host, err)
-	}
-
-	var respToken ACRAccessTokenResponse
-	if err := json.NewDecoder(resp.Body).Decode(&respToken); err != nil {
-		return nil, fmt.Errorf("%s: failed to decode access token response: %s",
-			host, err)
-	}
-
-	exp, err := getTokenExpiration(respToken.AccessToken)
-	if err != nil {
-		return nil, fmt.Errorf("%s: %s", host, err)
-	}
-
-	token := &adal.Token{
-		RefreshToken: c.RefreshToken,
-		AccessToken:  respToken.AccessToken,
-	}
-
-	client.Authorizer = autorest.NewBearerAuthorizer(token)
-
-	return &acrClient{
-		tokenExpiry: exp,
-		Client:      &client,
-	}, nil
-}
-
-func getTokenExpiration(tokenString string) (time.Time, error) {
-
-	token, err := jwt.Parse(tokenString, nil, jwt.WithoutClaimsValidation())
-	if err != nil {
-		return time.Time{}, err
-	}
-
-	claims, ok := token.Claims.(jwt.MapClaims)
-	if !ok {
-		return time.Time{}, fmt.Errorf("failed to process claims in access token")
-	}
-
-	if exp, ok := claims["exp"].(float64); ok {
-		timestamp := time.Unix(int64(exp), 0)
-		return timestamp, nil
-	}
-
-	return time.Time{}, fmt.Errorf("failed to find 'exp' claim in access token")
-}
diff --git a/pkg/client/acr/auth.go b/pkg/client/acr/auth.go
new file mode 100644
index 00000000..e8a2e651
--- /dev/null
+++ b/pkg/client/acr/auth.go
@@ -0,0 +1,153 @@
+package acr
+
+import (
+	"context"
+	"errors"
+	"time"
+
+	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+	"github.com/Azure/go-autorest/autorest"
+)
+
+type AuthOptions struct {
+	Username     string
+	Password     string
+	TenantID     string
+	AppID        string
+	ClientSecret string
+	RefreshToken string
+}
+
+func getAnonymousClient() *acrClient {
+	client := autorest.NewClientWithUserAgent(userAgent)
+	return &acrClient{
+		Client:      &client,
+		tokenExpiry: time.Unix(1<<63-1, 0),
+	}
+}
+
+func getServicePrincipalClient(ctx context.Context, opts AuthOptions, host string) (*acrClient, error) {
+	cred, err := azidentity.NewClientSecretCredential(opts.TenantID, opts.AppID, opts.ClientSecret, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	token, err := cred.GetToken(ctx, azidentity.TokenRequestOptions{
+		Scopes: []string{"https://" + host + "/.default"},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	client := autorest.NewClientWithUserAgent(userAgent)
+	client.Authorizer = autorest.NewBearerAuthorizer(&adal.Token{
+		AccessToken: token.Token,
+	})
+
+	return &acrClient{
+		Client:      &client,
+		tokenExpiry: token.ExpiresOn,
+	}, nil
+}
+
+func getBasicAuthClient(opts AuthOptions, host string) (*acrClient, error) {
+	client := autorest.NewClientWithUserAgent(userAgent)
+	client.Authorizer = autorest.NewBasicAuthorizer(opts.Username, opts.Password)
+
+	return &acrClient{
+		Client:      &client,
+		tokenExpiry: time.Unix(1<<63-1, 0),
+	}, nil
+}
+
+func getManagedIdentityClient(ctx context.Context, host string) (*acrClient, error) {
+	credential, err := azidentity.NewDefaultAzureCredential(nil)
+	if err != nil {
+		return nil, err
+	}
+
+	token, err := credential.GetToken(ctx, azidentity.TokenRequestOptions{
+		Scopes: []string{"https://" + host + "/.default"},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	client := autorest.NewClientWithUserAgent(userAgent)
+	client.Authorizer = autorest.NewBearerAuthorizer(&adal.Token{
+		AccessToken: token.Token,
+	})
+
+	return &acrClient{
+		Client:      &client,
+		tokenExpiry: token.ExpiresOn,
+	}, nil
+}
+
+func getAccessTokenClient(ctx context.Context, opts AuthOptions, host string) (*acrClient, error) {
+	client := autorest.NewClientWithUserAgent(userAgent)
+	urlParameters := map[string]interface{}{
+		"url": "https://" + host,
+	}
+
+	formDataParameters := map[string]interface{}{
+		"grant_type":    "refresh_token",
+		"refresh_token": opts.RefreshToken,
+		"scope":         "repository:*:*",
+		"service":       host,
+	}
+
+	preparer := autorest.CreatePreparer(
+		autorest.AsPost(),
+		autorest.WithCustomBaseURL("{url}", urlParameters),
+		autorest.WithPath("/oauth2/token"),
+		autorest.WithFormData(autorest.MapToValues(formDataParameters)))
+	req, err := preparer.Prepare((&http.Request{}).WithContext(ctx))
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := autorest.SendWithSender(client, req,
+		autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...))
+	if err != nil {
+		return nil, fmt.Errorf("%s: failed to request access token: %s", host, err)
+	}
+
+	var respToken ACRAccessTokenResponse
+	if err := json.NewDecoder(resp.Body).Decode(&respToken); err != nil {
+		return nil, fmt.Errorf("%s: failed to decode access token response: %s", host, err)
+	}
+
+	exp, err := getTokenExpiration(respToken.AccessToken)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %s", host, err)
+	}
+
+	client.Authorizer = autorest.NewBearerAuthorizer(&adal.Token{
+		AccessToken: respToken.AccessToken,
+	})
+
+	return &acrClient{
+		tokenExpiry: exp,
+		Client:      &client,
+	}, nil
+}
+
+func getTokenExpiration(tokenString string) (time.Time, error) {
+	token, err := jwt.Parse(tokenString, nil, jwt.WithoutClaimsValidation())
+	if err != nil {
+		return time.Time{}, err
+	}
+
+	claims, ok := token.Claims.(jwt.MapClaims)
+	if !ok {
+		return time.Time{}, fmt.Errorf("failed to process claims in access token")
+	}
+
+	if exp, ok := claims["exp"].(float64); ok {
+		timestamp := time.Unix(int64(exp), 0)
+		return timestamp, nil
+	}
+
+	return time.Time{}, fmt.Errorf("failed to find 'exp' claim in access token")
+}
diff --git a/pkg/client/acr/types.go b/pkg/client/acr/types.go
new file mode 100644
index 00000000..2eda43bc
--- /dev/null
+++ b/pkg/client/acr/types.go
@@ -0,0 +1,58 @@
+package acr
+
+import (
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/Azure/go-autorest/autorest"
+	"github.com/jetstack/version-checker/pkg/api"
+)
+
+const (
+	userAgent = "jetstack/version-checker"
+)
+
+type Client struct {
+	*http.Client
+	Options
+
+	cacheMu         sync.Mutex
+	cachedACRClient map[string]*acrClient
+}
+
+type acrClient struct {
+	tokenExpiry time.Time
+	// *azcontainerregistry.Client
+	*autorest.Client
+}
+
+type Options struct {
+	// Basic Auth
+	Username string
+	Password string
+	// Refresh Auth
+	RefreshToken string
+
+	TenantID     string
+	AppID        string
+	ClientSecret string
+}
+
+type ACRAccessTokenResponse struct {
+	AccessToken string `json:"access_token"`
+}
+
+type ACRManifestResponse struct {
+	Manifests []struct {
+		Digest       string           `json:"digest"`
+		CreatedTime  time.Time        `json:"createdTime"`
+		LastUpdated  time.Time        `json:"lastUpdateTime"`
+		Tags         []string         `json:"tags"`
+		Architecture api.Architecture `json:"architecture"`
+		OS           api.OS           `json:"os"`
+
+		MediaType       string `json:"mediaType"`
+		ConfigMediaType string `json:"configMediaType"`
+	} `json:"manifests"`
+}
diff --git a/pkg/client/docker/docker.go b/pkg/client/docker/docker.go
index 27b2dbb6..c9a072e7 100644
--- a/pkg/client/docker/docker.go
+++ b/pkg/client/docker/docker.go
@@ -18,38 +18,6 @@ const (
 	lookupURL = "https://registry.hub.docker.com/v2/repositories/%s/%s/tags?page_size=100"
 )
 
-type Options struct {
-	Username string
-	Password string
-	Token    string
-}
-
-type Client struct {
-	*http.Client
-	Options
-}
-
-type AuthResponse struct {
-	Token string `json:"token"`
-}
-
-type TagResponse struct {
-	Next    string   `json:"next"`
-	Results []Result `json:"results"`
-}
-
-type Result struct {
-	Name      string  `json:"name"`
-	Timestamp string  `json:"last_updated"`
-	Images    []Image `json:"images"`
-}
-
-type Image struct {
-	Digest       string           `json:"digest"`
-	OS           api.OS           `json:"os"`
-	Architecture api.Architecture `json:"Architecture"`
-}
-
 func New(ctx context.Context, opts Options) (*Client, error) {
 	client := &http.Client{
 		Timeout: time.Second * 10,
@@ -102,13 +70,19 @@ func (c *Client) Tags(ctx context.Context, _, repo, image string) ([]api.ImageTa
 				}
 			}
 
+			t := api.ImageTag{
+				Tag:       result.Name,
+				SHA:       result.Digest,
+				Timestamp: timestamp,
+			}
+
 			for _, image := range result.Images {
 				// Image without digest contains no real image.
 				if len(image.Digest) == 0 {
 					continue
 				}
 
-				tags = append(tags, api.ImageTag{
+				t.Children = append(t.Children, api.ImageTag{
 					Tag:          result.Name,
 					SHA:          image.Digest,
 					Timestamp:    timestamp,
@@ -116,6 +90,7 @@ func (c *Client) Tags(ctx context.Context, _, repo, image string) ([]api.ImageTa
 					Architecture: image.Architecture,
 				})
 			}
+			tags = append(tags, t)
 		}
 
 		url = response.Next
diff --git a/pkg/client/docker/docker_test.go b/pkg/client/docker/docker_test.go
new file mode 100644
index 00000000..17f4e704
--- /dev/null
+++ b/pkg/client/docker/docker_test.go
@@ -0,0 +1,226 @@
+package docker
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/jarcoal/httpmock"
+	"github.com/stretchr/testify/assert"
+)
+
+// Assuming Options, Client, AuthResponse, TagResponse, and other structs are defined in types.go
+
+func TestNew(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name        string
+		opts        Options
+		mockResp    string
+		mockStatus  int
+		expectError bool
+	}{
+		{
+			name: "Successful Auth",
+			opts: Options{
+				Username: "testuser",
+				Password: "testpassword",
+			},
+			mockResp:    `{"token": "testtoken"}`,
+			mockStatus:  http.StatusOK,
+			expectError: false,
+		},
+		{
+			name: "Auth Error",
+			opts: Options{
+				Username: "testuser",
+				Password: "wrongpassword",
+			},
+			mockResp:    `{"detail": "Invalid credentials"}`,
+			mockStatus:  http.StatusUnauthorized,
+			expectError: true,
+		},
+		{
+			name: "Token Specified with Username/Password",
+			opts: Options{
+				Username: "testuser",
+				Password: "testpassword",
+				Token:    "testtoken",
+			},
+			expectError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Start by activating httpmock
+			httpmock.Activate()
+			defer httpmock.DeactivateAndReset()
+
+			if tt.mockResp != "" {
+				httpmock.RegisterResponder("POST", loginURL,
+					httpmock.NewStringResponder(tt.mockStatus, tt.mockResp))
+			}
+
+			client, err := New(ctx, tt.opts)
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.NotNil(t, client)
+				if tt.opts.Token == "" {
+					assert.Equal(t, "testtoken", client.Options.Token)
+				}
+			}
+		})
+	}
+}
+
+func TestTags(t *testing.T) {
+	ctx := context.Background()
+	opts := Options{
+		Token: "testtoken",
+	}
+
+	client, err := New(ctx, opts)
+	assert.NoError(t, err)
+	assert.NotNil(t, client)
+
+	tests := []struct {
+		name        string
+		repo        string
+		image       string
+		mockResp    string
+		mockStatus  int
+		expectError bool
+		expectedLen int
+	}{
+		{
+			name:  "Successful Tags Fetch",
+			repo:  "testrepo",
+			image: "testimage",
+			mockResp: `{
+				"results": [{
+					"name": "v1.0",
+					"images": [{
+						"digest": "sha256:123",
+						"os": "linux",
+						"architecture": "amd64"
+					}],
+					"timestamp": "2021-01-01T00:00:00Z"
+				}],
+				"next": ""
+			}`,
+			mockStatus:  http.StatusOK,
+			expectError: false,
+			expectedLen: 1,
+		},
+		{
+			name:  "No Images",
+			repo:  "testrepo",
+			image: "noimages",
+			mockResp: `{
+				"results": [{
+					"name": "v1.0",
+					"images": [],
+					"timestamp": "2021-01-01T00:00:00Z"
+				}],
+				"next": ""
+			}`,
+			mockStatus:  http.StatusOK,
+			expectError: false,
+			expectedLen: 0,
+		},
+		{
+			name:        "API Error",
+			repo:        "testrepo",
+			image:       "errorimage",
+			mockResp:    `{"detail": "Not found"}`,
+			mockStatus:  http.StatusNotFound,
+			expectError: false,
+			expectedLen: 0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Start by activating httpmock
+			httpmock.Activate()
+			defer httpmock.DeactivateAndReset()
+
+			mockURL := fmt.Sprintf(lookupURL, tt.repo, tt.image)
+			httpmock.RegisterResponder("GET", mockURL,
+				httpmock.NewStringResponder(tt.mockStatus, tt.mockResp))
+
+			tags, err := client.Tags(ctx, "", tt.repo, tt.image)
+			if tt.expectError {
+				assert.Error(t, err)
+				assert.Nil(t, tags)
+			} else {
+				assert.NoError(t, err)
+				assert.Len(t, tags, tt.expectedLen)
+			}
+		})
+	}
+}
+
+func TestBasicAuthSetup(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name          string
+		opts          Options
+		mockResp      string
+		mockStatus    int
+		expectError   bool
+		expectedToken string
+	}{
+		{
+			name: "Successful Auth Setup",
+			opts: Options{
+				Username: "testuser",
+				Password: "testpassword",
+			},
+			mockResp:      `{"token": "testtoken"}`,
+			mockStatus:    http.StatusOK,
+			expectError:   false,
+			expectedToken: "testtoken",
+		},
+		{
+			name: "Auth Setup Error",
+			opts: Options{
+				Username: "testuser",
+				Password: "wrongpassword",
+			},
+			mockResp:    `{"detail": "Invalid credentials"}`,
+			mockStatus:  http.StatusUnauthorized,
+			expectError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Start by activating httpmock
+			httpmock.Activate()
+			defer httpmock.DeactivateAndReset()
+
+			httpmock.RegisterResponder("POST", loginURL,
+				httpmock.NewStringResponder(tt.mockStatus, tt.mockResp))
+
+			client := &http.Client{
+				Timeout: time.Second * 10,
+			}
+			token, err := basicAuthSetup(ctx, client, tt.opts)
+			if tt.expectError {
+				assert.Error(t, err)
+				assert.Empty(t, token)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.expectedToken, token)
+			}
+		})
+	}
+}
diff --git a/pkg/client/docker/types.go b/pkg/client/docker/types.go
new file mode 100644
index 00000000..5a2e1c8a
--- /dev/null
+++ b/pkg/client/docker/types.go
@@ -0,0 +1,40 @@
+package docker
+
+import (
+	"net/http"
+
+	"github.com/jetstack/version-checker/pkg/api"
+)
+
+type Options struct {
+	Username string
+	Password string
+	Token    string
+}
+
+type Client struct {
+	*http.Client
+	Options
+}
+
+type AuthResponse struct {
+	Token string `json:"token"`
+}
+
+type TagResponse struct {
+	Next    string   `json:"next"`
+	Results []Result `json:"results"`
+}
+
+type Result struct {
+	Name      string  `json:"name"`
+	Timestamp string  `json:"last_updated"`
+	Digest    string  `json:"digest"`
+	Images    []Image `json:"images"`
+}
+
+type Image struct {
+	Digest       string           `json:"digest"`
+	OS           api.OS           `json:"os"`
+	Architecture api.Architecture `json:"Architecture"`
+}
diff --git a/pkg/client/ecr/ecr.go b/pkg/client/ecr/ecr.go
index 3e70f4e4..f482bec1 100644
--- a/pkg/client/ecr/ecr.go
+++ b/pkg/client/ecr/ecr.go
@@ -2,12 +2,16 @@ package ecr
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"log"
 
 	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/config"
 	"github.com/aws/aws-sdk-go-v2/credentials"
 	"github.com/aws/aws-sdk-go-v2/service/ecr"
+	ecrtypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
+	"github.com/sirupsen/logrus"
 
 	"github.com/jetstack/version-checker/pkg/api"
 	"github.com/jetstack/version-checker/pkg/client/util"
@@ -65,13 +69,13 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.Imag
 	var tags []api.ImageTag
 	for _, img := range images.ImageDetails {
 
+		tags = append(tags, api.ImageTag{
+			SHA:       *img.ImageDigest,
+			Timestamp: *img.ImagePushedAt,
+		})
+
 		// Continue early if no tags available
 		if len(img.ImageTags) == 0 {
-			tags = append(tags, api.ImageTag{
-				SHA:       *img.ImageDigest,
-				Timestamp: *img.ImagePushedAt,
-			})
-
 			continue
 		}
 
@@ -83,6 +87,7 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.Imag
 			})
 		}
 	}
+	tags = getOSArchDetails(client, repoName, tags)
 
 	return tags, nil
 }
@@ -106,3 +111,109 @@ func (c *Client) createClient(ctx context.Context, region string) (*ecr.Client,
 	}
 	return ecr.NewFromConfig(cfg), nil
 }
+
+type ImageManifest struct {
+	Config struct {
+		Digest string `json:"digest"`
+	} `json:"config"`
+}
+
+type ImageConfig struct {
+	OS   string `json:"os"`
+	Arch string `json:"architecture"`
+}
+
+func getOSArchDetails(client *ecr.Client, repositoryName string, tags []api.ImageTag) []api.ImageTag {
+
+	// AWS only accept 100 tags at a time
+	tagGroups := splitTags(tags, 100)
+
+	for _, tags := range tagGroups {
+		var imageIds []ecrtypes.ImageIdentifier
+		for _, tag := range tags {
+			imageIds = append(imageIds, ecrtypes.ImageIdentifier{
+				ImageDigest: aws.String(tag.SHA),
+			})
+		}
+
+		manifestInput := &ecr.BatchGetImageInput{
+			RepositoryName:     aws.String(repositoryName),
+			ImageIds:           imageIds,
+			AcceptedMediaTypes: []string{"application/vnd.docker.distribution.manifest.v2+json"},
+		}
+
+		manifestOutput, err := client.BatchGetImage(context.Background(), manifestInput)
+		if err != nil {
+			log.Fatalf("failed to get image manifest: %v", err)
+		}
+
+		if len(manifestOutput.Images) > 0 {
+			manifest := manifestOutput.Images[0].ImageManifest
+
+			// Parse the image manifest
+			var imageManifest ImageManifest
+			err = json.Unmarshal([]byte(*manifest), &imageManifest)
+			if err != nil {
+				log.Fatalf("failed to unmarshal image manifest: %v", err)
+			}
+
+			// Get image configuration
+			configRequest := &ecr.BatchGetImageInput{
+				RepositoryName:     aws.String(repositoryName),
+				ImageIds:           imageIds,
+				AcceptedMediaTypes: []string{"application/vnd.docker.container.image.v1+json"},
+			}
+
+			configResponse, err := client.BatchGetImage(context.Background(), configRequest)
+			if err != nil {
+				log.Fatalf("failed to get image configuration: %v", err)
+			}
+
+			if len(configResponse.Images) > 0 {
+				for _, cfg := range configResponse.Images {
+
+					config := cfg.ImageManifest
+					sha := cfg.ImageId.ImageDigest
+
+					var imageConfig ImageConfig
+					err = json.Unmarshal([]byte(*config), &imageConfig)
+					if err != nil {
+						log.Fatalf("failed to unmarshal image configuration: %v", err)
+					}
+
+					// We need to go back through, ALL of our tags
+					// and enrich the OS and Architecture fields
+					found := false
+					for _, tag := range tags {
+						if tag.SHA == *sha || tag.Tag == *cfg.ImageId.ImageTag {
+							tag.OS = api.OS(imageConfig.OS) // Convert string to api.OS
+							tag.Architecture = api.Architecture(imageConfig.Arch)
+							found = true
+						}
+					}
+					if !found {
+						logrus.Warnf("failed to find ImageConfig for image digest: %s", *sha)
+						tags = append(tags, api.ImageTag{
+							SHA:          *sha,
+							Tag:          *cfg.ImageId.ImageTag,
+							OS:           api.OS(imageConfig.OS),
+							Architecture: api.Architecture(imageConfig.Arch),
+						})
+					}
+				}
+			}
+		}
+	}
+
+	return tags
+}
+
+// splitTags splits a slice of ImageTags into groups of a specified size
+func splitTags(slice []api.ImageTag, size int) [][]api.ImageTag {
+	var result [][]api.ImageTag
+	for size < len(slice) {
+		slice, result = slice[size:], append(result, slice[0:size:size])
+	}
+	result = append(result, slice)
+	return result
+}
diff --git a/pkg/client/gcr/gar.go b/pkg/client/gcr/gar.go
new file mode 100644
index 00000000..55c5c53b
--- /dev/null
+++ b/pkg/client/gcr/gar.go
@@ -0,0 +1,60 @@
+package gcr
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
+	"github.com/jetstack/version-checker/pkg/api"
+	"google.golang.org/api/iterator"
+)
+
+func (c *Client) listGARTags(ctx context.Context, host, repo, image string) ([]api.ImageTag, error) {
+	var tags []api.ImageTag
+
+	// Construct the parent path
+	parent := fmt.Sprintf("projects/%s/locations/%s/repositories/%s/dockerImages/%s", extractProjectID(repo), extractLocation(host), extractRepo(repo), image)
+
+	// Create the request
+	req := &artifactregistrypb.ListTagsRequest{
+		Parent: parent,
+	}
+
+	// Call the API to list docker images (manifests)
+	it := c.GAR.ListTags(ctx, req)
+	fmt.Println("GAR Tags:")
+	for {
+		tag, err := it.Next()
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		fmt.Println(tag.GetVersion())
+
+		// Get manifest details for each tag
+		// c.GAR.printGARManifestDetails(tag.GetVersion())
+	}
+
+	return tags, nil
+}
+
+func extractRepo(repo string) string {
+	parts := strings.Split(repo, "/")
+	return parts[1]
+}
+
+// Helper function to extract the project ID from the host
+func extractProjectID(repo string) string {
+	parts := strings.Split(repo, "/")
+	return parts[0]
+}
+
+// Helper function to extract the location from the host
+func extractLocation(host string) string {
+	parts := strings.Split(host, "-")
+	return strings.Join(parts[:len(parts)-1], "-")
+}
diff --git a/pkg/client/gcr/gcr.go b/pkg/client/gcr/gcr.go
index b539a94b..88df66fa 100644
--- a/pkg/client/gcr/gcr.go
+++ b/pkg/client/gcr/gcr.go
@@ -9,6 +9,7 @@ import (
 	"strconv"
 	"time"
 
+	"github.com/hashicorp/go-retryablehttp"
 	"github.com/jetstack/version-checker/pkg/api"
 )
 
@@ -16,15 +17,6 @@ const (
 	lookupURL = "https://%s/v2/%s/tags/list"
 )
 
-type Options struct {
-	Token string
-}
-
-type Client struct {
-	*http.Client
-	Options
-}
-
 type Response struct {
 	Manifest map[string]ManifestItem `json:"manifest"`
 }
@@ -54,18 +46,19 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.Imag
 
 	url := fmt.Sprintf(lookupURL, host, image)
 
-	req, err := http.NewRequest(http.MethodGet, url, nil)
+	req, err := retryablehttp.NewRequest(http.MethodGet, url, nil)
 	if err != nil {
 		return nil, err
 	}
 
+	// If we have a static token, we need to set it on each request.
 	if len(c.Token) > 0 {
 		req.SetBasicAuth("oauth2accesstoken", c.Token)
 	}
 
 	req = req.WithContext(ctx)
 
-	resp, err := c.Do(req)
+	resp, err := c.GCR.Do(req)
 	if err != nil {
 		return nil, fmt.Errorf("failed to get docker image: %s", err)
 	}
diff --git a/pkg/client/gcr/path.go b/pkg/client/gcr/path.go
index 69b678ea..0d401d65 100644
--- a/pkg/client/gcr/path.go
+++ b/pkg/client/gcr/path.go
@@ -20,6 +20,5 @@ func (c *Client) RepoImageFromPath(path string) (string, string) {
 	if lastIndex == -1 {
 		return "", path
 	}
-
 	return path[:lastIndex], path[lastIndex+1:]
 }
diff --git a/pkg/client/gcr/types.go b/pkg/client/gcr/types.go
new file mode 100644
index 00000000..e45c5081
--- /dev/null
+++ b/pkg/client/gcr/types.go
@@ -0,0 +1,70 @@
+package gcr
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/hashicorp/go-retryablehttp"
+	"github.com/jetstack/version-checker/pkg/api"
+	"golang.org/x/oauth2"
+	"google.golang.org/api/idtoken"
+	"google.golang.org/api/option"
+
+	artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
+)
+
+type Options struct {
+	Token string
+}
+
+type Client struct {
+	GAR *artifactregistry.Client
+	GCR *retryablehttp.Client
+	Options
+}
+
+func New(opts Options) *Client {
+	retryableClient := retryablehttp.NewClient()
+	retryableClient.RetryMax = 5                         // Set the number of retry attempts
+	retryableClient.HTTPClient.Timeout = 5 * time.Second // Set the HTTP client timeout
+	ctx := context.Background()
+	var garClient *artifactregistry.Client
+
+	if opts.Token == "" {
+		// Create an HTTP client with ID token authentication
+		idtokenClient, _ := idtoken.NewClient(ctx, "https://gcr.io")
+		retryableClient.HTTPClient = idtokenClient
+
+		// GAR Client by default does automatic token refresh
+		garClient, _ = artifactregistry.NewClient(ctx)
+	} else {
+		garClient, _ = artifactregistry.NewClient(ctx,
+			option.WithTokenSource(
+				oauth2.StaticTokenSource(&oauth2.Token{AccessToken: opts.Token})))
+	}
+
+	return &Client{
+		Options: opts,
+		GAR:     garClient,
+		GCR:     retryableClient,
+	}
+}
+
+func (c *Client) Name() string {
+	return "gcp"
+}
+
+func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.ImageTag, error) {
+
+	if strings.Contains(host, "gcr.io") {
+		// Handle GCR
+		return c.listGCRTags(ctx, host, repo, image)
+	} else if strings.Contains(host, "pkg.dev") {
+		// Handle GAR
+		return c.listGARTags(ctx, host, repo, image)
+	}
+
+	return nil, fmt.Errorf("unknown registry type for image path: %s/%s/%s", host, repo, image)
+}
diff --git a/pkg/client/ghcr/ghcr.go b/pkg/client/ghcr/ghcr.go
index 03e7f6ba..8a4aa567 100644
--- a/pkg/client/ghcr/ghcr.go
+++ b/pkg/client/ghcr/ghcr.go
@@ -9,6 +9,7 @@ import (
 	"github.com/gofri/go-github-ratelimit/github_ratelimit"
 	"github.com/google/go-github/v62/github"
 	"github.com/jetstack/version-checker/pkg/api"
+	"github.com/jetstack/version-checker/pkg/client/util"
 )
 
 type Options struct {
@@ -31,7 +32,11 @@ func New(opts Options) *Client {
 	if err != nil {
 		panic(err)
 	}
-	client := github.NewClient(ghRateLimiter).WithAuthToken(opts.Token)
+	client := github.NewClient(ghRateLimiter)
+	// Only add Auth Token if it is provided.
+	if len(opts.Token) > 0 {
+		client = client.WithAuthToken(opts.Token)
+	}
 
 	return &Client{
 		client:     client,
@@ -47,6 +52,7 @@ func (c *Client) Name() string {
 func (c *Client) Tags(ctx context.Context, host, owner, repo string) ([]api.ImageTag, error) {
 	// Choose the correct list packages function based on whether the owner
 	// is a user or an organization
+	// getReleases := c.Client.Repositories.ListReleases(ctx, owner, repo)
 	getAllVersions := c.client.Organizations.PackageGetAllVersions
 	ownerType, err := c.ownerType(ctx, owner)
 	if err != nil {
@@ -88,13 +94,7 @@ func (c *Client) Tags(ctx context.Context, host, owner, repo string) ([]api.Imag
 
 			for _, tag := range ver.Metadata.Container.Tags {
 				// Exclude attestations, signatures and sboms
-				if strings.HasSuffix(tag, ".att") {
-					continue
-				}
-				if strings.HasSuffix(tag, ".sig") {
-					continue
-				}
-				if strings.HasSuffix(tag, ".sbom") {
+				if util.FilterSbomAttestationSigs(tag) {
 					continue
 				}
 
diff --git a/pkg/client/ghcr/ghcr_test.go b/pkg/client/ghcr/ghcr_test.go
new file mode 100644
index 00000000..c8c687ae
--- /dev/null
+++ b/pkg/client/ghcr/ghcr_test.go
@@ -0,0 +1,167 @@
+package ghcr
+
+import (
+	"context"
+	"net/http"
+	"testing"
+
+	"github.com/google/go-github/v62/github"
+	"github.com/jarcoal/httpmock"
+	"github.com/stretchr/testify/assert"
+)
+
+func setup() {
+	httpmock.Activate()
+}
+
+func teardown() {
+	httpmock.DeactivateAndReset()
+}
+
+func registerCommonResponders() {
+	httpmock.RegisterResponder("GET", "https://api.github.com/users/test-user-owner",
+		func(req *http.Request) (*http.Response, error) {
+			return httpmock.NewStringResponse(200, `{"type":"User"}`), nil
+		})
+	httpmock.RegisterResponder("GET", "https://api.github.com/users/test-org-owner",
+		func(req *http.Request) (*http.Response, error) {
+			return httpmock.NewStringResponse(200, `{"type":"Organization"}`), nil
+		})
+}
+
+func registerTagResponders() {
+	httpmock.RegisterResponder("GET", "https://api.github.com/users/test-user-owner/packages/container/test-repo/versions",
+		func(req *http.Request) (*http.Response, error) {
+			return httpmock.NewStringResponse(200, `[
+				{
+					"name": "sha123",
+					"metadata": {
+						"container": {
+							"tags": ["tag1", "tag2"]
+						}
+					},
+					"created_at": "2023-07-08T12:34:56Z"
+				}
+			]`), nil
+		})
+	httpmock.RegisterResponder("GET", "https://api.github.com/orgs/test-org-owner/packages/container/test-repo/versions",
+		func(req *http.Request) (*http.Response, error) {
+			return httpmock.NewStringResponse(200, `[
+				{
+					"name": "sha123",
+					"metadata": {
+						"container": {
+							"tags": ["tag1", "tag2"]
+						}
+					},
+					"created_at": "2023-07-08T12:34:56Z"
+				}
+			]`), nil
+		})
+}
+
+func TestClient_Tags(t *testing.T) {
+	setup()
+	defer teardown()
+
+	ctx := context.Background()
+	host := "ghcr.io"
+
+	t.Run("successful tags fetch", func(t *testing.T) {
+		httpmock.Reset()
+		registerCommonResponders()
+		registerTagResponders()
+
+		client := New(Options{})
+		client.client = github.NewClient(nil) // Use the default HTTP client
+
+		tags, err := client.Tags(ctx, host, "test-user-owner", "test-repo")
+		assert.NoError(t, err)
+		assert.Len(t, tags, 2)
+		assert.Equal(t, "tag1", tags[0].Tag)
+		assert.Equal(t, "tag2", tags[1].Tag)
+	})
+
+	t.Run("failed to fetch owner type", func(t *testing.T) {
+		httpmock.Reset()
+		httpmock.RegisterResponder("GET", "https://api.github.com/users/test-user-owner",
+			func(req *http.Request) (*http.Response, error) {
+				return httpmock.NewStringResponse(404, `{"message": "Not Found"}`), nil
+			})
+
+		client := New(Options{})
+		client.client = github.NewClient(nil) // Use the default HTTP client
+
+		_, err := client.Tags(ctx, host, "test-user-owner", "test-repo")
+		assert.Error(t, err)
+	})
+
+	t.Run("token not set, no authorization header", func(t *testing.T) {
+		httpmock.Reset()
+		httpmock.RegisterResponder("GET", "https://api.github.com/users/test-user-owner",
+			func(req *http.Request) (*http.Response, error) {
+				if req.Header.Get("Authorization") != "" {
+					t.Errorf("expected no Authorization header, got %s", req.Header.Get("Authorization"))
+				}
+				return httpmock.NewStringResponse(200, `{"type":"User"}`), nil
+			})
+		registerTagResponders()
+
+		client := New(Options{}) // No token provided
+		client.client = github.NewClient(nil)
+
+		_, err := client.Tags(ctx, host, "test-user-owner", "test-repo")
+		assert.NoError(t, err)
+	})
+
+	t.Run("token set, authorization header sent", func(t *testing.T) {
+		token := "test-token"
+		httpmock.Reset()
+		httpmock.RegisterResponder("GET", "https://api.github.com/users/test-user-owner",
+			func(req *http.Request) (*http.Response, error) {
+				authHeader := req.Header.Get("Authorization")
+				expectedAuthHeader := "Bearer " + token
+				if authHeader != expectedAuthHeader {
+					t.Errorf("expected Authorization header %s, got %s", expectedAuthHeader, authHeader)
+				}
+				return httpmock.NewStringResponse(200, `{"type":"User"}`), nil
+			})
+
+		registerTagResponders()
+
+		client := New(Options{Token: token})
+
+		_, err := client.Tags(ctx, host, "test-user-owner", "test-repo")
+		assert.NoError(t, err)
+	})
+
+	t.Run("ownerType returns user", func(t *testing.T) {
+		httpmock.Reset()
+		registerCommonResponders()
+		registerTagResponders()
+
+		client := New(Options{})
+		client.client = github.NewClient(nil) // Use the default HTTP client
+
+		tags, err := client.Tags(ctx, host, "test-user-owner", "test-repo")
+		assert.NoError(t, err)
+		assert.Len(t, tags, 2)
+		assert.Equal(t, "tag1", tags[0].Tag)
+		assert.Equal(t, "tag2", tags[1].Tag)
+	})
+
+	t.Run("ownerType returns org", func(t *testing.T) {
+		httpmock.Reset()
+		registerCommonResponders()
+		registerTagResponders()
+
+		client := New(Options{})
+		client.client = github.NewClient(nil) // Use the default HTTP client
+
+		tags, err := client.Tags(ctx, host, "test-org-owner", "test-repo")
+		assert.NoError(t, err)
+		assert.Len(t, tags, 2)
+		assert.Equal(t, "tag1", tags[0].Tag)
+		assert.Equal(t, "tag2", tags[1].Tag)
+	})
+}
diff --git a/pkg/client/quay/quay.go b/pkg/client/quay/quay.go
index fce572bb..2c514f92 100644
--- a/pkg/client/quay/quay.go
+++ b/pkg/client/quay/quay.go
@@ -18,45 +18,6 @@ const (
 	manifestURL = "https://quay.io/api/v1/repository/%s/%s/manifest/%s"
 )
 
-type Options struct {
-	Token string
-}
-
-type Client struct {
-	*retryablehttp.Client
-	Options
-}
-
-type responseTag struct {
-	Tags          []responseTagItem `json:"tags"`
-	HasAdditional bool              `json:"has_additional"`
-	Page          int               `json:"page"`
-}
-
-type responseTagItem struct {
-	Name           string `json:"name"`
-	ManifestDigest string `json:"manifest_digest"`
-	LastModified   string `json:"last_modified"`
-	IsManifestList bool   `json:"is_manifest_list"`
-}
-
-type responseManifest struct {
-	ManifestData string `json:"manifest_data"`
-	Status       *int   `json:"status,omitempty"`
-}
-
-type responseManifestData struct {
-	Manifests []responseManifestDataItem `json:"manifests"`
-}
-
-type responseManifestDataItem struct {
-	Digest   string `json:"digest"`
-	Platform struct {
-		Architecture api.Architecture `json:"architecture"`
-		OS           api.OS           `json:"os"`
-	} `json:"platform"`
-}
-
 func New(opts Options) *Client {
 	client := retryablehttp.NewClient()
 	client.RetryMax = 10
@@ -89,6 +50,10 @@ func (c *Client) fetchImageManifest(ctx context.Context, repo, image string, tag
 	if err != nil {
 		return nil, err
 	}
+	// Filter Sbom, Attestations, Sigs
+	if util.FilterSbomAttestationSigs(tag.Name) {
+		return []api.ImageTag{}, nil
+	}
 
 	// If a multi-arch image, call manifest endpoint
 	if tag.IsManifestList {
@@ -135,6 +100,9 @@ func (c *Client) callManifests(ctx context.Context, timestamp time.Time, tag, ur
 
 	var tags []api.ImageTag
 	for _, manifest := range manifestData.Manifests {
+		if util.FilterSbomAttestationSigs(tag) {
+			continue
+		}
 		tags = append(tags, api.ImageTag{
 			Tag:          tag,
 			SHA:          manifest.Digest,
diff --git a/pkg/client/quay/types.go b/pkg/client/quay/types.go
new file mode 100644
index 00000000..0b0a7295
--- /dev/null
+++ b/pkg/client/quay/types.go
@@ -0,0 +1,45 @@
+package quay
+
+import (
+	"github.com/hashicorp/go-retryablehttp"
+	"github.com/jetstack/version-checker/pkg/api"
+)
+
+type Options struct {
+	Token string
+}
+
+type Client struct {
+	*retryablehttp.Client
+	Options
+}
+
+type responseTag struct {
+	Tags          []responseTagItem `json:"tags"`
+	HasAdditional bool              `json:"has_additional"`
+	Page          int               `json:"page"`
+}
+
+type responseTagItem struct {
+	Name           string `json:"name"`
+	ManifestDigest string `json:"manifest_digest"`
+	LastModified   string `json:"last_modified"`
+	IsManifestList bool   `json:"is_manifest_list"`
+}
+
+type responseManifest struct {
+	ManifestData string `json:"manifest_data"`
+	Status       *int   `json:"status,omitempty"`
+}
+
+type responseManifestData struct {
+	Manifests []responseManifestDataItem `json:"manifests"`
+}
+
+type responseManifestDataItem struct {
+	Digest   string `json:"digest"`
+	Platform struct {
+		Architecture api.Architecture `json:"architecture"`
+		OS           api.OS           `json:"os"`
+	} `json:"platform"`
+}
diff --git a/pkg/client/selfhosted/path_test.go b/pkg/client/selfhosted/path_test.go
index b78f6f13..79726d77 100644
--- a/pkg/client/selfhosted/path_test.go
+++ b/pkg/client/selfhosted/path_test.go
@@ -105,6 +105,16 @@ func TestRepoImage(t *testing.T) {
 			expRepo:  "joshvanl",
 			expImage: "version-checker",
 		},
+		"multiple nested segments to path should return last two": {
+			path:     "folder1/subfolder/subsubfolder/image",
+			expRepo:  "folder1/subfolder/subsubfolder",
+			expImage: "image",
+		},
+		"multiple nested segments to paths should return last two": {
+			path:     "folder1/subfolder/subsubfolder/subsubsubfolder/image",
+			expRepo:  "folder1/subfolder/subsubfolder/subsubsubfolder",
+			expImage: "image",
+		},
 	}
 
 	handler := new(Client)
diff --git a/pkg/client/selfhosted/selfhosted.go b/pkg/client/selfhosted/selfhosted.go
index da891638..a2ab8538 100644
--- a/pkg/client/selfhosted/selfhosted.go
+++ b/pkg/client/selfhosted/selfhosted.go
@@ -10,10 +10,10 @@ import (
 	"io"
 	"net/http"
 	"os"
-	"regexp"
 	"strings"
 	"time"
 
+	"github.com/hashicorp/go-retryablehttp"
 	"github.com/sirupsen/logrus"
 
 	"github.com/jetstack/version-checker/pkg/api"
@@ -30,57 +30,21 @@ const (
 	defaultTokenPath = "/v2/token"
 
 	// HTTP headers to request API version
-	dockerAPIv1Header = "application/vnd.docker.distribution.manifest.v1+json"
-	dockerAPIv2Header = "application/vnd.docker.distribution.manifest.v2+json"
+	dockerAPIv1Header   = "application/vnd.docker.distribution.manifest.v1+json"
+	dockerAPIv2Header   = "application/vnd.docker.distribution.manifest.v2+json"
+	ociImageIndexHeader = "application/vnd.oci.image.index.v1+json"
 )
 
-type Options struct {
-	Host      string
-	Username  string
-	Password  string
-	Bearer    string
-	TokenPath string
-	Insecure  bool
-	CAPath    string
-}
-
-type Client struct {
-	*http.Client
-	*Options
-
-	log *logrus.Entry
-
-	hostRegex  *regexp.Regexp
-	httpScheme string
-}
-
-type AuthResponse struct {
-	Token string `json:"token"`
-}
-
-type TagResponse struct {
-	Tags []string `json:"tags"`
-}
+func New(ctx context.Context, log *logrus.Entry, opts *Options) (*Client, error) {
 
-type ManifestResponse struct {
-	Digest       string
-	Architecture api.Architecture `json:"architecture"`
-	History      []History        `json:"history"`
-}
+	retryClient := retryablehttp.NewClient()
+	retryClient.RetryMax = opts.RetryMax
 
-type History struct {
-	V1Compatibility string `json:"v1Compatibility"`
-}
-
-type V1Compatibility struct {
-	Created time.Time `json:"created,omitempty"`
-}
+	stdClient := retryClient.StandardClient()
+	stdClient.Timeout = time.Second * time.Duration(opts.Timeout)
 
-func New(ctx context.Context, log *logrus.Entry, opts *Options) (*Client, error) {
 	client := &Client{
-		Client: &http.Client{
-			Timeout: time.Second * 10,
-		},
+		Client:  stdClient,
 		Options: opts,
 		log:     log.WithField("client", opts.Host),
 	}
@@ -162,8 +126,14 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.Imag
 	for _, tag := range tagResponse.Tags {
 		manifestURL := fmt.Sprintf(manifestPath, host, path, tag)
 
+		// Exclude attestations, signatures and sboms
+		if util.FilterSbomAttestationSigs(tag) {
+			continue
+		}
+
 		var manifestResponse ManifestResponse
-		_, err := c.doRequest(ctx, manifestURL, dockerAPIv1Header, &manifestResponse)
+		headers := strings.Join([]string{dockerAPIv1Header, ociImageIndexHeader}, ",")
+		_, err := c.doRequest(ctx, manifestURL, headers, &manifestResponse)
 
 		if httpErr, ok := selfhostederrors.IsHTTPError(err); ok {
 			c.log.Errorf("%s: failed to get manifest response for tag, skipping (%d): %s",
@@ -189,7 +159,8 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.Imag
 			}
 		}
 
-		header, err := c.doRequest(ctx, manifestURL, dockerAPIv2Header, new(ManifestResponse))
+		headers = strings.Join([]string{dockerAPIv2Header, ociImageIndexHeader}, ",")
+		header, err := c.doRequest(ctx, manifestURL, headers, new(ManifestResponse))
 		if httpErr, ok := selfhostederrors.IsHTTPError(err); ok {
 			c.log.Errorf("%s: failed to get manifest sha response for tag, skipping (%d): %s",
 				manifestURL, httpErr.StatusCode, httpErr.Body)
@@ -296,7 +267,7 @@ func newTLSConfig(insecure bool, CAPath string) (*tls.Config, error) {
 	if CAPath != "" {
 		certs, err := os.ReadFile(CAPath)
 		if err != nil {
-			return nil, fmt.Errorf("Failed to append %q to RootCAs: %v", CAPath, err)
+			return nil, fmt.Errorf("failed to append %q to RootCAs: %v", CAPath, err)
 		}
 		rootCAs.AppendCertsFromPEM(certs)
 	}
diff --git a/pkg/client/selfhosted/types.go b/pkg/client/selfhosted/types.go
new file mode 100644
index 00000000..c1f670ea
--- /dev/null
+++ b/pkg/client/selfhosted/types.go
@@ -0,0 +1,54 @@
+package selfhosted
+
+import (
+	"net/http"
+	"regexp"
+	"time"
+
+	"github.com/jetstack/version-checker/pkg/api"
+	"github.com/sirupsen/logrus"
+)
+
+type Options struct {
+	Host      string
+	Username  string
+	Password  string
+	Bearer    string
+	TokenPath string
+	Insecure  bool
+	Timeout   int
+	RetryMax  int
+	CAPath    string
+}
+
+type Client struct {
+	*http.Client
+	*Options
+
+	log *logrus.Entry
+
+	hostRegex  *regexp.Regexp
+	httpScheme string
+}
+
+type AuthResponse struct {
+	Token string `json:"token"`
+}
+
+type TagResponse struct {
+	Tags []string `json:"tags"`
+}
+
+type ManifestResponse struct {
+	Digest       string
+	Architecture api.Architecture `json:"architecture"`
+	History      []History        `json:"history"`
+}
+
+type History struct {
+	V1Compatibility string `json:"v1Compatibility"`
+}
+
+type V1Compatibility struct {
+	Created time.Time `json:"created,omitempty"`
+}
diff --git a/pkg/client/util/util.go b/pkg/client/util/util.go
index 14ece724..3394df9f 100644
--- a/pkg/client/util/util.go
+++ b/pkg/client/util/util.go
@@ -7,14 +7,14 @@ import (
 )
 
 var (
-	oss = [...]api.OS{
+	KnownOSs = [...]api.OS{
 		"linux",
 		"darwin",
 		"windows",
 		"freebsd",
 	}
 
-	archs = [...]api.Architecture{
+	KnownArchs = [...]api.Architecture{
 		"amd",
 		"amd64",
 		"arm",
@@ -56,13 +56,13 @@ func OSArchFromTag(tag string) (api.OS, api.Architecture) {
 	for _, s := range split {
 		ss := strings.ToLower(s)
 
-		for _, pos := range oss {
+		for _, pos := range KnownOSs {
 			if pos == api.OS(ss) {
 				os = pos
 			}
 		}
 
-		for _, parch := range archs {
+		for _, parch := range KnownArchs {
 			if parch == api.Architecture(ss) {
 				arch = parch
 			}
@@ -71,3 +71,7 @@ func OSArchFromTag(tag string) (api.OS, api.Architecture) {
 
 	return os, arch
 }
+
+func FilterSbomAttestationSigs(tag string) bool {
+	return strings.HasSuffix(tag, ".att") || strings.HasSuffix(tag, ".sig") || strings.HasSuffix(tag, ".sbom")
+}
diff --git a/pkg/controller/checker/checker_test.go b/pkg/controller/checker/checker_test.go
index e9beb774..5276765e 100644
--- a/pkg/controller/checker/checker_test.go
+++ b/pkg/controller/checker/checker_test.go
@@ -7,14 +7,40 @@ import (
 
 	"github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/jetstack/version-checker/pkg/api"
+	"github.com/jetstack/version-checker/pkg/controller/checker/architecture"
 	"github.com/jetstack/version-checker/pkg/controller/internal/fake/search"
 	"github.com/jetstack/version-checker/pkg/version/semver"
 )
 
 func TestContainer(t *testing.T) {
+	// Create a NodeMap and pre-fill it with expected nodes
+	nodeMap := architecture.New()
+
+	// Add nodes with expected OS and architecture
+	nodeMap.Add(&corev1.Node{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "node-amd64",
+			Labels: map[string]string{
+				corev1.LabelOSStable:   "linux",
+				corev1.LabelArchStable: "amd64",
+			},
+		},
+	})
+	nodeMap.Add(&corev1.Node{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "node-arm64",
+			Labels: map[string]string{
+				corev1.LabelOSStable:   "linux",
+				corev1.LabelArchStable: "arm64",
+			},
+		},
+	})
+
 	tests := map[string]struct {
+		nodeName   string
 		statusSHA  string
 		imageURL   string
 		opts       *api.Options
@@ -22,6 +48,7 @@ func TestContainer(t *testing.T) {
 		expResult  *Result
 	}{
 		"no status sha should return nil, nil": {
+			nodeName:   "node-amd64",
 			statusSHA:  "",
 			imageURL:   "version-checker:v0.2.0",
 			opts:       nil,
@@ -29,226 +56,134 @@ func TestContainer(t *testing.T) {
 			expResult:  nil,
 		},
 		"if v0.2.0 is latest version, but different sha, then not latest": {
+			nodeName:  "node-amd64",
 			statusSHA: "localhost:5000/version-checker@sha:123",
 			imageURL:  "localhost:5000/version-checker:v0.2.0",
 			opts:      new(api.Options),
 			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:456",
+				Tag:          "v0.2.0",
+				SHA:          "sha:456",
+				OS:           "linux",
+				Architecture: "amd64",
 			},
 			expResult: &Result{
 				CurrentVersion: "v0.2.0@sha:123",
 				LatestVersion:  "v0.2.0@sha:456",
 				ImageURL:       "localhost:5000/version-checker",
 				IsLatest:       false,
+				OS:             "linux",
+				Architecture:   "amd64",
 			},
 		},
 		"if v0.2.0 is latest version, but same sha, then latest": {
+			nodeName:  "node-amd64",
 			statusSHA: "localhost:5000/version-checker@sha:123",
 			imageURL:  "localhost:5000/version-checker:v0.2.0",
 			opts:      new(api.Options),
 			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:123",
+				Tag:          "v0.2.0",
+				SHA:          "sha:123",
+				OS:           "linux",
+				Architecture: "amd64",
 			},
 			expResult: &Result{
 				CurrentVersion: "v0.2.0",
 				LatestVersion:  "v0.2.0",
 				ImageURL:       "localhost:5000/version-checker",
 				IsLatest:       true,
+				OS:             "linux",
+				Architecture:   "amd64",
 			},
 		},
 		"if v0.2.0@sha:123 is wrong sha, then not latest": {
+			nodeName:  "node-amd64",
 			statusSHA: "localhost:5000/version-checker@sha:123",
 			imageURL:  "localhost:5000/version-checker:v0.2.0@sha:123",
 			opts:      new(api.Options),
 			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:456",
+				Tag:          "v0.2.0",
+				SHA:          "sha:456",
+				OS:           "linux",
+				Architecture: "amd64",
 			},
 			expResult: &Result{
 				CurrentVersion: "v0.2.0@sha:123",
 				LatestVersion:  "v0.2.0@sha:456",
 				ImageURL:       "localhost:5000/version-checker",
 				IsLatest:       false,
+				OS:             "linux",
+				Architecture:   "amd64",
 			},
 		},
 		"if v0.2.0@sha:123 is correct sha, then latest": {
+			nodeName:  "node-amd64",
 			statusSHA: "localhost:5000/version-checker@sha:123",
 			imageURL:  "localhost:5000/version-checker:v0.2.0@sha:123",
 			opts:      new(api.Options),
 			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:123",
+				Tag:          "v0.2.0",
+				SHA:          "sha:123",
+				OS:           "linux",
+				Architecture: "amd64",
 			},
 			expResult: &Result{
 				CurrentVersion: "v0.2.0@sha:123",
 				LatestVersion:  "v0.2.0@sha:123",
 				ImageURL:       "localhost:5000/version-checker",
 				IsLatest:       true,
+				OS:             "linux",
+				Architecture:   "amd64",
 			},
 		},
-		"if empty is not latest version, then return false": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
-			imageURL:  "localhost:5000/version-checker",
-			opts:      new(api.Options),
-			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:456",
-			},
-			expResult: &Result{
-				CurrentVersion: "sha:123",
-				LatestVersion:  "v0.2.0@sha:456",
-				ImageURL:       "localhost:5000/version-checker",
-				IsLatest:       false,
-			},
-		},
-		"if empty is latest version, then return true": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
-			imageURL:  "localhost:5000/version-checker",
-			opts:      new(api.Options),
-			searchResp: &api.ImageTag{
-				Tag: "",
-				SHA: "sha:123",
-			},
-			expResult: &Result{
-				CurrentVersion: "sha:123",
-				LatestVersion:  "sha:123",
-				ImageURL:       "localhost:5000/version-checker",
-				IsLatest:       true,
-			},
-		},
-		"if latest is not latest version, then return false": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
-			imageURL:  "localhost:5000/version-checker:latest",
-			opts:      new(api.Options),
-			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:456",
-			},
-			expResult: &Result{
-				CurrentVersion: "sha:123",
-				LatestVersion:  "v0.2.0@sha:456",
-				ImageURL:       "localhost:5000/version-checker",
-				IsLatest:       false,
-			},
-		},
-		"if latest is latest version, then return true": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
-			imageURL:  "localhost:5000/version-checker:latest",
-			opts:      new(api.Options),
-			searchResp: &api.ImageTag{
-				Tag: "",
-				SHA: "sha:123",
-			},
-			expResult: &Result{
-				CurrentVersion: "sha:123",
-				LatestVersion:  "sha:123",
-				ImageURL:       "localhost:5000/version-checker",
-				IsLatest:       true,
-			},
-		},
-		"if using v0.2.0 with use sha, but not latest, return false": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
+		"if arm64 node with v0.2.0 is latest version, but different sha, then not latest": {
+			nodeName:  "node-arm64",
+			statusSHA: "localhost:5000/version-checker@sha:789",
 			imageURL:  "localhost:5000/version-checker:v0.2.0",
-			opts: &api.Options{
-				UseSHA: true,
-			},
+			opts:      new(api.Options),
 			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:456",
+				Tag:          "v0.2.0",
+				SHA:          "sha:101112",
+				OS:           "linux",
+				Architecture: "arm64",
 			},
 			expResult: &Result{
-				CurrentVersion: "v0.2.0@sha:123",
-				LatestVersion:  "v0.2.0@sha:456",
+				CurrentVersion: "v0.2.0@sha:789",
+				LatestVersion:  "v0.2.0@sha:101112",
 				ImageURL:       "localhost:5000/version-checker",
 				IsLatest:       false,
+				OS:             "linux",
+				Architecture:   "arm64",
 			},
 		},
-		"if using v0.2.0 with use sha, but latest, return true": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
+		"if arm64 node with v0.2.0 is latest version, but same sha, then latest": {
+			nodeName:  "node-arm64",
+			statusSHA: "localhost:5000/version-checker@sha:789",
 			imageURL:  "localhost:5000/version-checker:v0.2.0",
-			opts: &api.Options{
-				UseSHA: true,
-			},
-			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:123",
-			},
-			expResult: &Result{
-				CurrentVersion: "v0.2.0@sha:123",
-				LatestVersion:  "v0.2.0@sha:123",
-				ImageURL:       "localhost:5000/version-checker",
-				IsLatest:       true,
-			},
-		},
-		"if using sha but not latest, return false": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
-			imageURL:  "localhost:5000/joshvanl/version-checker@sha:123",
-			opts:      new(api.Options),
-			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:456",
-			},
-			expResult: &Result{
-				CurrentVersion: "sha:123",
-				LatestVersion:  "v0.2.0@sha:456",
-				ImageURL:       "localhost:5000/joshvanl/version-checker",
-				IsLatest:       false,
-			},
-		},
-		"if using sha but sha not latest, return false and no tag if non exists": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
-			imageURL:  "localhost:5000/joshvanl/version-checker@sha:123",
-			opts:      new(api.Options),
-			searchResp: &api.ImageTag{
-				Tag: "",
-				SHA: "sha:456",
-			},
-			expResult: &Result{
-				CurrentVersion: "sha:123",
-				LatestVersion:  "sha:456",
-				ImageURL:       "localhost:5000/joshvanl/version-checker",
-				IsLatest:       false,
-			},
-		},
-		"if using sha and is latest, return true": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
-			imageURL:  "localhost:5000/joshvanl/version-checker@sha:123",
 			opts:      new(api.Options),
 			searchResp: &api.ImageTag{
-				Tag: "v0.2.0",
-				SHA: "sha:123",
+				Tag:          "v0.2.0",
+				SHA:          "sha:789",
+				OS:           "linux",
+				Architecture: "arm64",
 			},
 			expResult: &Result{
-				CurrentVersion: "sha:123",
-				LatestVersion:  "v0.2.0@sha:123",
-				ImageURL:       "localhost:5000/joshvanl/version-checker",
-				IsLatest:       true,
-			},
-		},
-		"if using sha and is latest, return true and no tag if non exists": {
-			statusSHA: "localhost:5000/version-checker@sha:123",
-			imageURL:  "localhost:5000/joshvanl/version-checker@sha:123",
-			opts:      new(api.Options),
-			searchResp: &api.ImageTag{
-				Tag: "",
-				SHA: "sha:123",
-			},
-			expResult: &Result{
-				CurrentVersion: "sha:123",
-				LatestVersion:  "sha:123",
-				ImageURL:       "localhost:5000/joshvanl/version-checker",
+				CurrentVersion: "v0.2.0",
+				LatestVersion:  "v0.2.0",
+				ImageURL:       "localhost:5000/version-checker",
 				IsLatest:       true,
+				OS:             "linux",
+				Architecture:   "arm64",
 			},
 		},
 	}
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
-
-			checker := New(search.New().With(test.searchResp, nil))
+			checker := New(search.New().With(test.searchResp, nil), nodeMap)
 			pod := &corev1.Pod{
+				Spec: corev1.PodSpec{
+					NodeName: test.nodeName,
+				},
 				Status: corev1.PodStatus{
 					ContainerStatuses: []corev1.ContainerStatus{
 						{
@@ -362,6 +297,7 @@ func TestContainerStatusImageSHA(t *testing.T) {
 }
 
 func TestIsLatestOrEmptyTag(t *testing.T) {
+	dummyNodeMap := &architecture.NodeMap{}
 	tests := map[string]struct {
 		tag   string
 		expIs bool
@@ -382,7 +318,7 @@ func TestIsLatestOrEmptyTag(t *testing.T) {
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
-			checker := New(search.New())
+			checker := New(search.New(), dummyNodeMap)
 			if is := checker.isLatestOrEmptyTag(test.tag); is != test.expIs {
 				t.Errorf("unexpected isLatestOrEmptyTag exp=%t got=%t",
 					test.expIs, is)
@@ -392,6 +328,7 @@ func TestIsLatestOrEmptyTag(t *testing.T) {
 }
 
 func TestIsLatestSemver(t *testing.T) {
+	dummyNodeMap := &architecture.NodeMap{}
 	tests := map[string]struct {
 		imageURL, currentSHA string
 		currentImage         *semver.SemVer
@@ -459,7 +396,7 @@ func TestIsLatestSemver(t *testing.T) {
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
-			checker := New(search.New().With(test.searchResp, nil))
+			checker := New(search.New().With(test.searchResp, nil), dummyNodeMap)
 			latestImage, isLatest, err := checker.isLatestSemver(context.TODO(), test.imageURL, test.currentSHA, test.currentImage, nil)
 			if err != nil {
 				t.Fatal(err)
@@ -479,6 +416,7 @@ func TestIsLatestSemver(t *testing.T) {
 }
 
 func TestIsLatestSHA(t *testing.T) {
+	dummyNodeMap := &architecture.NodeMap{}
 	tests := map[string]struct {
 		imageURL, currentSHA string
 		searchResp           *api.ImageTag
@@ -514,7 +452,7 @@ func TestIsLatestSHA(t *testing.T) {
 
 	for name, test := range tests {
 		t.Run(name, func(t *testing.T) {
-			checker := New(search.New().With(test.searchResp, nil))
+			checker := New(search.New().With(test.searchResp, nil), dummyNodeMap)
 			result, err := checker.isLatestSHA(context.TODO(), test.imageURL, test.currentSHA, nil)
 			if err != nil {
 				t.Fatal(err)
diff --git a/pkg/controller/sync.go b/pkg/controller/sync.go
index d0e8351f..750bf695 100644
--- a/pkg/controller/sync.go
+++ b/pkg/controller/sync.go
@@ -40,11 +40,24 @@ func (c *Controller) sync(ctx context.Context, pod *corev1.Pod) error {
 	return nil
 }
 
+func isPodRunnindOrReady(pod *corev1.Pod) bool {
+	if pod.Status.Phase != corev1.PodRunning {
+		return false
+	}
+
+	for _, condition := range pod.Status.Conditions {
+		if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue {
+			return true
+		}
+	}
+	return false
+}
+
 // syncContainer will enqueue a given container to check the version
 func (c *Controller) syncContainer(ctx context.Context, log *logrus.Entry, builder *options.Builder, pod *corev1.Pod,
 	container *corev1.Container, containerType string) error {
-	// If not enabled, exit early
-	if !builder.IsEnabled(c.defaultTestAll, container.Name) {
+	// If not enabled, or pod not Ready/Running exit early
+	if !builder.IsEnabled(c.defaultTestAll, container.Name) || !isPodRunnindOrReady(pod) {
 		c.metrics.RemoveImage(pod.Namespace, pod.Name, container.Name, containerType)
 		return nil
 	}
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index b32ab86a..5c88dd6d 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -12,6 +12,7 @@ import (
 	"github.com/sirupsen/logrus"
 
 	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 )
 
@@ -53,7 +54,7 @@ type Entry struct {
 }
 
 func New(log *logrus.Entry) *Metrics {
-	containerImageVersion := prometheus.NewGaugeVec(
+	containerImageVersion := promauto.NewGaugeVec(
 		prometheus.GaugeOpts{
 			Namespace: "version_checker",
 			Name:      "is_latest_version",
@@ -64,12 +65,8 @@ func New(log *logrus.Entry) *Metrics {
 		},
 	)
 
-	registry := prometheus.NewRegistry()
-	registry.MustRegister(containerImageVersion)
-
 	return &Metrics{
 		log:                   log.WithField("module", "metrics"),
-		registry:              registry,
 		containerImageVersion: containerImageVersion,
 		containerCache:        make(map[string]cacheItem),
 	}
@@ -78,7 +75,7 @@ func New(log *logrus.Entry) *Metrics {
 // Run will run the metrics server
 func (m *Metrics) Run(servingAddress string) error {
 	router := http.NewServeMux()
-	router.Handle("/metrics", promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}))
+	router.Handle("/metrics", promhttp.Handler())
 	router.Handle("/healthz", http.HandlerFunc(m.healthzAndReadyzHandler))
 	router.Handle("/readyz", http.HandlerFunc(m.healthzAndReadyzHandler))
 
@@ -140,21 +137,30 @@ func (m *Metrics) RemoveImage(namespace, pod, container, containerType string) {
 	index := m.latestImageIndex(namespace, pod, container, containerType)
 	item, ok := m.containerCache[index]
 	if !ok {
+		m.log.Warnf("RemoveImage: no cache item found for %s", index)
 		return
 	}
 
-	m.containerImageVersion.Delete(
-		m.buildLabels(&Entry{
-			Namespace:      namespace,
-			Pod:            pod,
-			Container:      container,
-			ImageURL:       item.image,
-			CurrentVersion: item.currentVersion,
-			LatestVersion:  item.latestVersion,
-			OS:             item.os,
-			Arch:           item.arch,
-		}),
-	)
+	m.log.Infof("Removing metric with labels: namespace=%s, pod=%s, container=%s, containerType=%s, image=%s, currentVersion=%s, latestVersion=%s, os=%s, arch=%s",
+		namespace, pod, container, containerType, item.image, item.currentVersion, item.latestVersion, item.os, item.arch)
+
+	labels := m.buildLabels(&Entry{
+		Namespace:      namespace,
+		Pod:            pod,
+		Container:      container,
+		ContainerType:  containerType,
+		ImageURL:       item.image,
+		CurrentVersion: item.currentVersion,
+		LatestVersion:  item.latestVersion,
+		OS:             item.os,
+		Arch:           item.arch,
+	})
+
+	deleted := m.containerImageVersion.Delete(labels)
+	if !deleted {
+		m.log.Warnf("Failed to delete metric with labels: %v", labels)
+	}
+
 	delete(m.containerCache, index)
 }
 
diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go
index 518d1780..b843565a 100644
--- a/pkg/metrics/metrics_test.go
+++ b/pkg/metrics/metrics_test.go
@@ -1,7 +1,6 @@
 package metrics
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/prometheus/client_golang/prometheus/testutil"
@@ -9,31 +8,110 @@ import (
 )
 
 func TestCache(t *testing.T) {
-	m := New(logrus.NewEntry(logrus.New()))
+	logger := logrus.NewEntry(logrus.New())
+	m := New(logger)
 
-	for i, typ := range []string{"init", "container"} {
-		version := fmt.Sprintf("0.1.%d", i)
-		m.AddImage("namespace", "pod", "container", typ, "url", true, version, version)
+	type testCase struct {
+		name          string
+		namespace     string
+		pod           string
+		container     string
+		containerType string
+		version       string
+		expectCount   float64
+		action        func(*Metrics, *Entry)
 	}
 
-	for i, typ := range []string{"init", "container"} {
-		version := fmt.Sprintf("0.1.%d", i)
-		mt, _ := m.containerImageVersion.GetMetricWith(m.buildLabels("namespace", "pod", "container", typ, "url", version, version))
-		count := testutil.ToFloat64(mt)
-		if count != 1 {
-			t.Error("Should have added metric")
-		}
+	tests := []testCase{
+		{
+			name:          "Add init container metric",
+			namespace:     "namespace",
+			pod:           "pod",
+			container:     "container",
+			containerType: "init",
+			version:       "0.1.0",
+			expectCount:   1,
+			action: func(m *Metrics, lbs *Entry) {
+				m.AddImage(lbs)
+			},
+		},
+		{
+			name:          "Add container metric",
+			namespace:     "namespace",
+			pod:           "pod",
+			container:     "container",
+			containerType: "container",
+			version:       "0.1.1",
+			expectCount:   1,
+			action: func(m *Metrics, lbs *Entry) {
+				m.AddImage(lbs)
+			},
+		},
+		{
+			name:          "Remove init container metric",
+			namespace:     "namespace",
+			pod:           "pod",
+			container:     "container",
+			containerType: "init",
+			version:       "0.1.0",
+			expectCount:   0,
+			action: func(m *Metrics, lbs *Entry) {
+				m.RemoveImage(lbs.Namespace, lbs.Pod, lbs.Container, lbs.ContainerType)
+			},
+		},
+		{
+			name:          "Remove container metric",
+			namespace:     "namespace",
+			pod:           "pod",
+			container:     "container",
+			containerType: "container",
+			version:       "0.1.1",
+			expectCount:   0,
+			action: func(m *Metrics, lbs *Entry) {
+				m.RemoveImage(lbs.Namespace, lbs.Pod, lbs.Container, lbs.ContainerType)
+			},
+		},
 	}
 
-	for _, typ := range []string{"init", "container"} {
-		m.RemoveImage("namespace", "pod", "container", typ)
-	}
-	for i, typ := range []string{"init", "container"} {
-		version := fmt.Sprintf("0.1.%d", i)
-		mt, _ := m.containerImageVersion.GetMetricWith(m.buildLabels("namespace", "pod", "container", typ, "url", version, version))
-		count := testutil.ToFloat64(mt)
-		if count != 0 {
-			t.Error("Should have removed metric")
-		}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			lbs := &Entry{
+				Namespace:      tt.namespace,
+				Pod:            tt.pod,
+				Container:      tt.container,
+				ContainerType:  tt.containerType,
+				ImageURL:       "url",
+				CurrentVersion: tt.version,
+				LatestVersion:  tt.version,
+				OS:             "",
+				Arch:           "",
+				IsLatest:       true,
+			}
+
+			// Perform the action
+			tt.action(m, lbs)
+
+			// Verify the metric count
+			mt, err := m.containerImageVersion.GetMetricWith(m.buildLabels(lbs))
+			if err != nil {
+				t.Fatalf("Error getting metric: %v", err)
+			}
+			count := testutil.ToFloat64(mt)
+			if count != tt.expectCount {
+				t.Errorf("expected metric count %v, got %v", tt.expectCount, count)
+			}
+
+			// Clean up by ensuring the metric is removed
+			m.RemoveImage(lbs.Namespace, lbs.Pod, lbs.Container, lbs.ContainerType)
+			mt, err = m.containerImageVersion.GetMetricWith(m.buildLabels(lbs))
+			if err == nil {
+				finalCount := testutil.ToFloat64(mt)
+				if finalCount != 0 {
+					t.Errorf("expected final metric count 0, got %v", finalCount)
+				}
+			} else if tt.expectCount != 0 {
+				t.Errorf("expected to find metric but got error: %v", err)
+			}
+		})
 	}
 }
diff --git a/pkg/version/utils.go b/pkg/version/utils.go
index a5ffafb4..def204ba 100644
--- a/pkg/version/utils.go
+++ b/pkg/version/utils.go
@@ -16,7 +16,7 @@ func latestSemver(tags []api.ImageTag, opts *api.Options) (*api.ImageTag, error)
 	)
 
 	for i := range tags {
-		// forcing it be the specific arch and os (defalts to true, if not set)
+		// forcing it be the specific arch and os (defaults to true, if not set)
 		if !osArchMatch(tags[i], opts) {
 			continue
 		}

From 2afc644977a55e03d0836e0629703e51fc92408f Mon Sep 17 00:00:00 2001
From: David Collom <david.collom@jetstack.io>
Date: Tue, 9 Jul 2024 11:13:20 +0100
Subject: [PATCH 3/3] Fix margo replace

---
 go.mod | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index e6e6e255..718b24a2 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,8 @@ toolchain go1.22.3
 // please place any replace statements here at the top for visibility and add a
 // comment to it as to when it can be removed
 
+replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
+
 require (
 	github.com/Azure/go-autorest/autorest v0.11.29
 	github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect
@@ -99,7 +101,7 @@ require (
 	github.com/googleapis/gax-go/v2 v2.12.5 // indirect
 	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
-	github.com/imdario/mergo v0.3.16 // indirect
+	github.com/imdario/mergo v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
