From dfaf915ef8129c998d27666723d9a41353fb7632 Mon Sep 17 00:00:00 2001 From: David Collom Date: Tue, 9 Jul 2024 10:54:12 +0100 Subject: [PATCH] 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 }