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/deploy/charts/version-checker/templates/clusterrole.yaml b/deploy/charts/version-checker/templates/clusterrole.yaml index c9d54df7..63c1be3e 100644 --- a/deploy/charts/version-checker/templates/clusterrole.yaml +++ b/deploy/charts/version-checker/templates/clusterrole.yaml @@ -9,6 +9,7 @@ rules: - "" resources: - "pods" + - "nodes" verbs: - "get" - "list" diff --git a/deploy/charts/version-checker/values.yaml b/deploy/charts/version-checker/values.yaml index 132ca626..84ecb46d 100644 --- a/deploy/charts/version-checker/values.yaml +++ b/deploy/charts/version-checker/values.yaml @@ -108,7 +108,6 @@ selfhosted: # password: bar # token: - # -- Setup version-checkers resource requests/limits resources: {} @@ -121,7 +120,8 @@ resources: # # -- Set container-level security context -securityContext: {} +securityContext: + {} # allowPrivilegeEscalation: false # capabilities: # drop: diff --git a/deploy/yaml/deploy.yaml b/deploy/yaml/deploy.yaml index aac17973..6e7d40cf 100644 --- a/deploy/yaml/deploy.yaml +++ b/deploy/yaml/deploy.yaml @@ -49,33 +49,33 @@ spec: spec: serviceAccountName: version-checker containers: - - image: quay.io/jetstack/version-checker:v0.7.0 - imagePullPolicy: Always - ports: - - containerPort: 8080 - name: version-checker - command: ["version-checker"] - livenessProbe: - httpGet: - path: /readyz - port: 8080 - initialDelaySeconds: 3 - periodSeconds: 3 - readinessProbe: - httpGet: - path: /readyz - port: 8080 - initialDelaySeconds: 3 - periodSeconds: 3 + - image: quay.io/jetstack/version-checker:v0.7.0 + imagePullPolicy: Always + ports: + - containerPort: 8080 + name: version-checker + command: ["version-checker"] + livenessProbe: + httpGet: + path: /readyz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /readyz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: version-checker rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get", "watch", "list"] + - apiGroups: [""] + resources: ["pods", "nodes"] + verbs: ["get", "watch", "list"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 @@ -86,6 +86,6 @@ roleRef: kind: ClusterRole name: version-checker subjects: -- kind: ServiceAccount - name: version-checker - namespace: version-checker + - kind: ServiceAccount + name: version-checker + namespace: version-checker diff --git a/go.mod b/go.mod index d02133b9..ec1cefb5 100644 --- a/go.mod +++ b/go.mod @@ -8,17 +8,18 @@ toolchain go1.22.3 // please place any replace statements here at the top for visibility and add a // comment to it as to when it can be removed +replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16 + require ( github.com/Azure/go-autorest/autorest v0.11.29 - github.com/Azure/go-autorest/autorest/adal v0.9.24 - github.com/aws/aws-sdk-go-v2 v1.30.0 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect + github.com/aws/aws-sdk-go-v2 v1.30.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,65 +29,89 @@ require ( ) require ( - github.com/aws/aws-sdk-go-v2/config v1.27.21 - github.com/aws/aws-sdk-go-v2/credentials v1.17.21 - github.com/aws/aws-sdk-go-v2/service/ecr v1.29.1 + cloud.google.com/go/artifactregistry v1.14.11 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 + github.com/aws/aws-sdk-go-v2/config v1.27.24 + github.com/aws/aws-sdk-go-v2/credentials v1.17.24 + github.com/aws/aws-sdk-go-v2/service/ecr v1.30.1 github.com/gofri/go-github-ratelimit v1.1.0 - github.com/google/go-containerregistry v0.19.0 - github.com/google/go-github/v58 v58.0.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + 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 + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd ) 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/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/azure/cli v0.4.6 // 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/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 // indirect + github.com/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 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 // indirect - github.com/aws/smithy-go v1.20.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect + github.com/aws/smithy-go v1.20.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v24.0.0+incompatible // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v24.0.9+incompatible // indirect - github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/docker/cli v27.0.3+incompatible // indirect + github.com/docker/distribution v2.8.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/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 + github.com/imdario/mergo v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.5 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/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 @@ -96,31 +121,41 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/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.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/vbatts/tar-split v0.11.3 // 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.starlark.net v0.0.0-20240520160348-046347dcd104 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/net v0.26.0 // indirect + go.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 golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect + google.golang.org/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 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f // indirect sigs.k8s.io/kustomize/api v0.17.2 // indirect sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect diff --git a/go.sum b/go.sum index 6bd77fa9..b8f0a588 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,39 @@ +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= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= @@ -16,44 +43,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/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/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/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/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/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/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.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.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.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.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.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/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/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/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/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/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/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/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.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.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.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.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.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.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/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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.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= @@ -61,24 +92,33 @@ 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/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/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-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= -github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +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.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-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= @@ -93,24 +133,45 @@ github.com/gofri/go-github-ratelimit v1.1.0/go.mod h1:OnCi5gV+hAG/LMR7llGhU7yHt4 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= 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-github/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw= -github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4= +github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg= +github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -118,10 +179,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= @@ -134,6 +202,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= @@ -144,12 +214,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= @@ -158,6 +230,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= @@ -177,10 +251,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= @@ -188,18 +264,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.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -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/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.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.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= @@ -219,16 +295,27 @@ 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= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -238,25 +325,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= @@ -264,6 +363,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= @@ -274,19 +374,19 @@ 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= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -299,6 +399,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= @@ -310,6 +414,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= @@ -327,6 +460,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= @@ -339,8 +474,8 @@ 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= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/pkg/api/types.go b/pkg/api/types.go index b7a6cbb0..fe36079d 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -58,6 +58,10 @@ type Options struct { PinPatch *int64 `json:"pin-patch,omitempty"` RegexMatcher *regexp.Regexp `json:"-"` + + // Architecture and OS to search for + Architecture *Architecture `json:"pin-architecture,omitempty"` + OS *OS `json:"pin-os,omitempty"` } // ImageTag describes a container image tag. @@ -67,7 +71,52 @@ type ImageTag struct { Timestamp time.Time `json:"timestamp"` OS OS `json:"os,omitempty"` Architecture Architecture `json:"architecture,omitempty"` + Children []ImageTag `json:"children,omitempty"` } type OS string 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..33815019 --- /dev/null +++ b/pkg/client/acr/auth.go @@ -0,0 +1,152 @@ +package acr + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/go-autorest/autorest" + "github.com/golang-jwt/jwt/v5" +) + +const userAgent = "jetstack/version-checker" + +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 + } + + tokenFunc := func(something, host string) (string, error) { + token, err := cred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{"https://" + host + "/.default"}, + }) + if err != nil { + return "", err + } + return token.Token, nil + } + + client := autorest.NewClientWithUserAgent(userAgent) + auth := autorest.NewBearerAuthorizerCallback(client, autorest.BearerAuthorizerCallbackFunc(tokenFunc)) + client.Authorizer = auth + + return &acrClient{ + Client: &client, + tokenExpiry: time.Now().Add(time.Hour), // Assuming 1 hour validity for the token + }, 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) { + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, err + } + + tokenFunc := func(somthing, host string) (string, error) { + token, err := cred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{"https://" + host + "/.default"}, + }) + if err != nil { + return "", err + } + return token.Token, nil + } + + client := autorest.NewClientWithUserAgent(userAgent) + auth := autorest.NewBearerAuthorizerCallback(client, autorest.BearerAuthorizerCallbackFunc(tokenFunc)) + client.Authorizer = auth + + return &acrClient{ + Client: &client, + tokenExpiry: time.Now().Add(time.Hour), // Assuming 1 hour validity for the token + }, nil +} + +func getAccessTokenClient(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 + } + + tokenFunc := func() (string, error) { + token, err := cred.GetToken(ctx, policy.TokenRequestOptions{ + Scopes: []string{"https://" + host + "/.default"}, + }) + if err != nil { + return "", fmt.Errorf("%s: failed to request access token: %s", host, err) + } + return token.Token, nil + } + + client := autorest.NewClientWithUserAgent(userAgent) + auth := autorest.NewBearerAuthorizerCallback(client, autorest.BearerAuthorizerCallbackFunc(tokenFunc)) + client.Authorizer = auth + + return &acrClient{ + Client: &client, + tokenExpiry: time.Now().Add(time.Hour), // Assuming 1 hour validity for the token + }, 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") +} + +type acrClient struct { + token azcore.AccessToken + tokenExpiry time.Time + Client *autorest.Client +} + +func (c *acrClient) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) { + if time.Now().After(c.tokenExpiry) { + return azcore.AccessToken{}, fmt.Errorf("token expired") + } + return c.token, nil +} diff --git a/pkg/client/acr/types.go b/pkg/client/acr/types.go new file mode 100644 index 00000000..00137c56 --- /dev/null +++ b/pkg/client/acr/types.go @@ -0,0 +1,55 @@ +package acr + +import ( + "net/http" + "sync" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/go-autorest/autorest" + "github.com/jetstack/version-checker/pkg/api" +) + +type Client struct { + *http.Client + Options + + cacheMu sync.Mutex + cachedACRClient map[string]*acrClient +} + +type acrClient struct { + token azcore.AccessToken + tokenExpiry time.Time + 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..e6423589 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"` } @@ -34,19 +26,6 @@ type ManifestItem struct { TimeCreated string `json:"timeCreatedMs"` } -func New(opts Options) *Client { - return &Client{ - Options: opts, - Client: &http.Client{ - Timeout: time.Second * 5, - }, - } -} - -func (c *Client) Name() string { - return "gcr" -} - func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.ImageTag, error) { if repo != "" { image = fmt.Sprintf("%s/%s", repo, image) @@ -54,18 +33,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 169a12f0..8a4aa567 100644 --- a/pkg/client/ghcr/ghcr.go +++ b/pkg/client/ghcr/ghcr.go @@ -7,8 +7,9 @@ import ( "strings" "github.com/gofri/go-github-ratelimit/github_ratelimit" - "github.com/google/go-github/v58/github" + "github.com/google/go-github/v62/github" "github.com/jetstack/version-checker/pkg/api" + "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 ca6031e9..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 { @@ -102,7 +67,6 @@ func (c *Client) fetchImageManifest(ctx context.Context, repo, image string, tag } // Fallback to not using multi-arch image - os, arch := util.OSArchFromTag(tag.Name) return []api.ImageTag{ @@ -136,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/architecture/architecture.go b/pkg/controller/checker/architecture/architecture.go new file mode 100644 index 00000000..4f025518 --- /dev/null +++ b/pkg/controller/checker/architecture/architecture.go @@ -0,0 +1,74 @@ +package architecture + +import ( + "errors" + "fmt" + "sync" + + corev1 "k8s.io/api/core/v1" + + "github.com/jetstack/version-checker/pkg/api" + "github.com/sirupsen/logrus" +) + +// NodeMetadata metadata about a particular node +type nodeMetadata struct { + OS api.OS + Architecture api.Architecture +} + +type NodeMap struct { + mu sync.RWMutex + nodes map[string]*nodeMetadata +} + +func New() *NodeMap { + // might need to pass an initial map + return &NodeMap{ + nodes: make(map[string]*nodeMetadata), + } +} + +func (m *NodeMap) GetArchitecture(nodeName string) (*nodeMetadata, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + + meta, ok := m.nodes[nodeName] + return meta, ok +} + +func (m *NodeMap) Add(node *corev1.Node) error { + m.mu.Lock() + defer m.mu.Unlock() + if node == nil { + return errors.New("passed node is nil") + } + + arch, ok := node.Labels[corev1.LabelArchStable] + if !ok { + return fmt.Errorf("missing %q label on node %q", corev1.LabelArchStable, node.Name) + } + + os, ok := node.Labels[corev1.LabelOSStable] + if !ok { + return fmt.Errorf("missing %q label on node %q", corev1.LabelOSStable, node.Name) + } + + logrus.WithField("module", "architecture"). + WithField("node", node.Name). + WithField("os", os). + WithField("arch", arch). + Debug("node successfully added") + + m.nodes[node.Name] = &nodeMetadata{ + OS: api.OS(os), + Architecture: api.Architecture(arch), + } + return nil +} + +func (m *NodeMap) Delete(nodeName string) { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.nodes, nodeName) +} diff --git a/pkg/controller/checker/checker.go b/pkg/controller/checker/checker.go index 1ba6b147..47ce066d 100644 --- a/pkg/controller/checker/checker.go +++ b/pkg/controller/checker/checker.go @@ -2,12 +2,14 @@ package checker import ( "context" + "errors" "fmt" "strings" corev1 "k8s.io/api/core/v1" "github.com/jetstack/version-checker/pkg/api" + "github.com/jetstack/version-checker/pkg/controller/checker/architecture" "github.com/jetstack/version-checker/pkg/controller/search" "github.com/jetstack/version-checker/pkg/version/semver" "github.com/sirupsen/logrus" @@ -15,6 +17,7 @@ import ( type Checker struct { search search.Searcher + nodes *architecture.NodeMap } type Result struct { @@ -22,11 +25,18 @@ type Result struct { LatestVersion string IsLatest bool ImageURL string + OS api.OS + Architecture api.Architecture } -func New(search search.Searcher) *Checker { +const ( + nodeMissingMetadata = "error fetching node architecture" +) + +func New(search search.Searcher, nodesArchInfo *architecture.NodeMap) *Checker { return &Checker{ search: search, + nodes: nodesArchInfo, } } @@ -34,6 +44,20 @@ func New(search search.Searcher) *Checker { func (c *Checker) Container(ctx context.Context, log *logrus.Entry, pod *corev1.Pod, container *corev1.Container, opts *api.Options) (*Result, error) { + if opts == nil { + // create new options if nil + opts = new(api.Options) + } + + // Get information about the pod node architecture + arch, ok := c.nodes.GetArchitecture(pod.Spec.NodeName) + if ok { + opts.OS = &arch.OS + opts.Architecture = &arch.Architecture + } else { + return nil, errors.New(nodeMissingMetadata) + } + // If the container image SHA status is not ready yet, exit early statusSHA := containerStatusImageSHA(pod, container.Name) if len(statusSHA) == 0 { @@ -90,6 +114,8 @@ func (c *Checker) Container(ctx context.Context, log *logrus.Entry, LatestVersion: latestVersion, IsLatest: isLatest, ImageURL: imageURL, + OS: latestImage.OS, + Architecture: latestImage.Architecture, }, nil } @@ -174,6 +200,8 @@ func (c *Checker) isLatestSHA(ctx context.Context, imageURL, currentSHA string, LatestVersion: latestVersion, IsLatest: isLatest, ImageURL: imageURL, + OS: latestImage.OS, + Architecture: latestImage.Architecture, }, nil } diff --git a/pkg/controller/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/controller.go b/pkg/controller/controller.go index 2f63cec4..ea840180 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -18,6 +18,8 @@ import ( "github.com/jetstack/version-checker/pkg/client" "github.com/jetstack/version-checker/pkg/controller/checker" + "github.com/jetstack/version-checker/pkg/controller/checker/architecture" + "github.com/jetstack/version-checker/pkg/controller/nodes" "github.com/jetstack/version-checker/pkg/controller/scheduler" "github.com/jetstack/version-checker/pkg/controller/search" "github.com/jetstack/version-checker/pkg/metrics" @@ -38,6 +40,9 @@ type Controller struct { workqueue workqueue.RateLimitingInterface scheduledWorkQueue scheduler.ScheduledWorkQueue + nodesArchitecture *architecture.NodeMap + nodeController *nodes.NodeInformer + metrics *metrics.Metrics checker *checker.Checker @@ -56,17 +61,23 @@ func New( scheduledWorkQueue := scheduler.NewScheduledWorkQueue(clock.RealClock{}, workqueue.Add) log = log.WithField("module", "controller") + nodesArchitecture := architecture.New() + versionGetter := version.New(log, imageClient, cacheTimeout) search := search.New(log, cacheTimeout, versionGetter) + nodeController := nodes.New(log, nodesArchitecture) + c := &Controller{ log: log, kubeClient: kubeClient, workqueue: workqueue, scheduledWorkQueue: scheduledWorkQueue, metrics: metrics, - checker: checker.New(search), + checker: checker.New(search, nodesArchitecture), defaultTestAll: defaultTestAll, + nodesArchitecture: nodesArchitecture, + nodeController: nodeController, } return c @@ -78,6 +89,8 @@ func (c *Controller) Run(ctx context.Context, cacheRefreshRate time.Duration) er sharedInformerFactory := informers.NewSharedInformerFactoryWithOptions(c.kubeClient, time.Second*30) c.podLister = sharedInformerFactory.Core().V1().Pods().Lister() + nodeInformer := c.nodeController.Register(sharedInformerFactory) + podInformer := sharedInformerFactory.Core().V1().Pods().Informer() podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: c.addObject, @@ -92,7 +105,7 @@ func (c *Controller) Run(ctx context.Context, cacheRefreshRate time.Duration) er c.log.Info("starting control loop") sharedInformerFactory.Start(ctx.Done()) - if !cache.WaitForCacheSync(ctx.Done(), podInformer.HasSynced) { + if !cache.WaitForCacheSync(ctx.Done(), nodeInformer.HasSynced, podInformer.HasSynced) { return fmt.Errorf("error waiting for informer caches to sync") } diff --git a/pkg/controller/nodes/nodes.go b/pkg/controller/nodes/nodes.go new file mode 100644 index 00000000..a09c346a --- /dev/null +++ b/pkg/controller/nodes/nodes.go @@ -0,0 +1,56 @@ +package nodes + +import ( + "github.com/jetstack/version-checker/pkg/controller/checker/architecture" + "github.com/sirupsen/logrus" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/informers" + "k8s.io/client-go/tools/cache" +) + +// NodeInformer is the wrapper for the k8s Node Informer +type NodeInformer struct { + log *logrus.Entry + nodes *architecture.NodeMap +} + +// New returns a new instance of NodeInformer +func New(log *logrus.Entry, nodes *architecture.NodeMap) *NodeInformer { + return &NodeInformer{ + log: log.WithField("module", "controller_node"), + nodes: nodes, + } +} + +// Register returns the node informer with event handler set to update node architecture map +func (c *NodeInformer) Register(sharedInformerFactory informers.SharedInformerFactory) cache.SharedIndexInformer { + nodeInformer := sharedInformerFactory.Core().V1().Nodes().Informer() + nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + // Add node info and data to the map + err := c.nodes.Add(obj.(*corev1.Node)) + if err != nil { + c.log.Errorf("error adding the node %q to architecture map: %s", obj.(*corev1.Node), err) + return + } + c.log.Debugf("added node %q to architecture map", obj.(*corev1.Node).Name) + }, + UpdateFunc: func(old, new interface{}) { + // override the map + err := c.nodes.Add(new.(*corev1.Node)) + if err != nil { + c.log.Errorf("error updating the node %q in architecture map: %s", old.(*corev1.Node), err) + return + } + c.log.Debugf("updated node %q in architecture map", old.(*corev1.Node).Name) + }, + DeleteFunc: func(obj interface{}) { + // remove node from the map + c.nodes.Delete((obj.(*corev1.Node)).Name) + c.log.Debugf("removed node %q from architecture map", obj.(*corev1.Node).Name) + }, + }) + + return nodeInformer +} diff --git a/pkg/controller/search/search.go b/pkg/controller/search/search.go index 43e3086c..d4c291cd 100644 --- a/pkg/controller/search/search.go +++ b/pkg/controller/search/search.go @@ -79,10 +79,10 @@ func calculateHashIndex(imageURL string, opts *api.Options) (string, error) { return "", fmt.Errorf("failed to marshal options: %s", err) } - hash := fnv.New32() + hash := fnv.New64() if _, err := hash.Write(append(optsJSON, []byte(imageURL)...)); err != nil { return "", fmt.Errorf("failed to calculate search hash: %s", err) } - return fmt.Sprintf("%d", hash.Sum32()), nil + return fmt.Sprintf("%d", hash.Sum64()), nil } diff --git a/pkg/controller/sync.go b/pkg/controller/sync.go index 6b5a33ae..750bf695 100644 --- a/pkg/controller/sync.go +++ b/pkg/controller/sync.go @@ -10,6 +10,7 @@ import ( "github.com/jetstack/version-checker/pkg/api" "github.com/jetstack/version-checker/pkg/controller/options" + "github.com/jetstack/version-checker/pkg/metrics" versionerrors "github.com/jetstack/version-checker/pkg/version/errors" ) @@ -39,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 } @@ -86,18 +100,25 @@ func (c *Controller) checkContainer(ctx context.Context, log *logrus.Entry, pod } if result.IsLatest { - log.Debugf("image is latest %s:%s", - result.ImageURL, result.CurrentVersion) + log.Debugf("image is latest %s [%s/%s]:%s", + result.ImageURL, result.OS, result.Architecture, result.CurrentVersion) } else { - log.Debugf("image is not latest %s: %s -> %s", - result.ImageURL, result.CurrentVersion, result.LatestVersion) + log.Debugf("image is not latest %s [%s/%s]: %s -> %s", + result.ImageURL, result.OS, result.Architecture, result.CurrentVersion, result.LatestVersion) } - c.metrics.AddImage(pod.Namespace, pod.Name, - container.Name, containerType, - result.ImageURL, result.IsLatest, - result.CurrentVersion, result.LatestVersion, - ) + c.metrics.AddImage(&metrics.Entry{ + Namespace: pod.Namespace, + Pod: pod.Name, + Container: container.Name, + ContainerType: containerType, + ImageURL: result.ImageURL, + IsLatest: result.IsLatest, + CurrentVersion: result.CurrentVersion, + LatestVersion: result.LatestVersion, + OS: string(result.OS), + Arch: string(result.Architecture), + }) return nil } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 58b10e70..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" ) @@ -34,26 +35,38 @@ type cacheItem struct { image string currentVersion string latestVersion string + os string + arch string +} + +// Entry is a struct containing a single metrics label set +type Entry struct { + Namespace string + Pod string + Container string + ContainerType string + ImageURL string + IsLatest bool + CurrentVersion string + LatestVersion string + OS string + Arch string } func New(log *logrus.Entry) *Metrics { - containerImageVersion := prometheus.NewGaugeVec( + containerImageVersion := promauto.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "version_checker", Name: "is_latest_version", Help: "Where the container in use is using the latest upstream registry version", }, []string{ - "namespace", "pod", "container", "container_type", "image", "current_version", "latest_version", + "namespace", "pod", "container", "container_type", "image", "current_version", "latest_version", "architecture", "os", }, ) - registry := prometheus.NewRegistry() - registry.MustRegister(containerImageVersion) - return &Metrics{ log: log.WithField("module", "metrics"), - registry: registry, containerImageVersion: containerImageVersion, containerCache: make(map[string]cacheItem), } @@ -62,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)) @@ -91,27 +104,29 @@ func (m *Metrics) Run(servingAddress string) error { return nil } -func (m *Metrics) AddImage(namespace, pod, container, containerType, imageURL string, isLatest bool, currentVersion, latestVersion string) { +func (m *Metrics) AddImage(entry *Entry) { // Remove old image url/version if it exists - m.RemoveImage(namespace, pod, container, containerType) + m.RemoveImage(entry.Namespace, entry.Pod, entry.Container, entry.ContainerType) m.mu.Lock() defer m.mu.Unlock() isLatestF := 0.0 - if isLatest { + if entry.IsLatest { isLatestF = 1.0 } m.containerImageVersion.With( - m.buildLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion), + m.buildLabels(entry), ).Set(isLatestF) - index := m.latestImageIndex(namespace, pod, container, containerType) + index := m.latestImageIndex(entry.Namespace, entry.Pod, entry.Container, entry.ContainerType) m.containerCache[index] = cacheItem{ - image: imageURL, - currentVersion: currentVersion, - latestVersion: latestVersion, + image: entry.ImageURL, + currentVersion: entry.CurrentVersion, + latestVersion: entry.LatestVersion, + os: entry.OS, + arch: entry.Arch, } } @@ -122,12 +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(namespace, pod, container, containerType, item.image, item.currentVersion, item.latestVersion), - ) + 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) } @@ -135,15 +168,17 @@ func (m *Metrics) latestImageIndex(namespace, pod, container, containerType stri return strings.Join([]string{namespace, pod, container, containerType}, "") } -func (m *Metrics) buildLabels(namespace, pod, container, containerType, imageURL, currentVersion, latestVersion string) prometheus.Labels { +func (m *Metrics) buildLabels(entry *Entry) prometheus.Labels { return prometheus.Labels{ - "namespace": namespace, - "pod": pod, - "container_type": containerType, - "container": container, - "image": imageURL, - "current_version": currentVersion, - "latest_version": latestVersion, + "namespace": entry.Namespace, + "pod": entry.Pod, + "container": entry.Container, + "container_type": entry.ContainerType, + "image": entry.ImageURL, + "current_version": entry.CurrentVersion, + "latest_version": entry.LatestVersion, + "architecture": entry.Arch, + "os": entry.OS, } } diff --git a/pkg/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 new file mode 100644 index 00000000..3d7fa022 --- /dev/null +++ b/pkg/version/utils.go @@ -0,0 +1,88 @@ +package version + +import ( + "github.com/jetstack/version-checker/pkg/api" + "github.com/jetstack/version-checker/pkg/version/semver" +) + +// latestSemver will return the latest ImageTag based on the given options +// restriction, using semver. This should not be used is UseSHA has been +// enabled. +func latestSemver(tags []api.ImageTag, opts *api.Options) (*api.ImageTag, error) { + var ( + latestImageTag *api.ImageTag + latestV *semver.SemVer + ) + + for i := range tags { + // forcing it be the specific arch and os (defaults to true, if not set) + if !matchOSandArch(tags[i], opts) { + continue + } + + v := semver.Parse(tags[i].Tag) + + // If regex enabled continue here. + // If we match, and is less than, update latest. + if opts.RegexMatcher != nil { + if opts.RegexMatcher.MatchString(tags[i].Tag) && + (latestV == nil || latestV.LessThan(v)) { + latestV = v + latestImageTag = &tags[i] + } + + continue + } + + // If we have declared we wont use metadata but version has it, continue. + if !opts.UseMetaData && v.HasMetaData() { + continue + } + + if opts.PinMajor != nil && *opts.PinMajor != v.Major() { + continue + } + if opts.PinMinor != nil && *opts.PinMinor != v.Minor() { + continue + } + if opts.PinPatch != nil && *opts.PinPatch != v.Patch() { + continue + } + + // If no latest yet set + if latestV == nil || + // If the latest set is less than + latestV.LessThan(v) || + // If the latest is the same tag, but smaller timestamp + (latestV.Equal(v) && tags[i].Timestamp.After(latestImageTag.Timestamp)) { + latestV = v + latestImageTag = &tags[i] + } + } + + return latestImageTag, nil +} + +// latestSHA will return the latest ImageTag based on image timestamps. +func latestSHA(tags []api.ImageTag, opts *api.Options) (*api.ImageTag, error) { + var latestTag *api.ImageTag + + for i := range tags { + // forcing it be the specific arch and os (defalts to true, if not set) + if !matchOSandArch(tags[i], opts) { + continue + } + if latestTag == nil || tags[i].Timestamp.After(latestTag.Timestamp) { + latestTag = &tags[i] + } + } + + return latestTag, nil +} + +func matchOSandArch(tag api.ImageTag, opts *api.Options) bool { + if opts.OS == nil || opts.Architecture == nil { + return true + } + return tag.OS == *opts.OS && tag.Architecture == *opts.Architecture +} diff --git a/pkg/version/version_test.go b/pkg/version/utils_test.go similarity index 99% rename from pkg/version/version_test.go rename to pkg/version/utils_test.go index 6f744ecd..972d38a2 100644 --- a/pkg/version/version_test.go +++ b/pkg/version/utils_test.go @@ -193,7 +193,7 @@ func TestLatestSemver(t *testing.T) { if len(tt.tags) > 0 { tags = tt.tags } - tag, err := latestSemver(tt.opts, tags) + tag, err := latestSemver(tags, tt.opts) assert.NoError(t, err) assert.NotNil(t, tag) assert.Equal(t, tt.expected, tag.Tag) diff --git a/pkg/version/version.go b/pkg/version/version.go index 05b7cb7c..35996c9c 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -13,7 +13,6 @@ import ( "github.com/jetstack/version-checker/pkg/cache" versionerrors "github.com/jetstack/version-checker/pkg/version/errors" - "github.com/jetstack/version-checker/pkg/version/semver" ) type Version struct { @@ -48,6 +47,7 @@ func (v *Version) LatestTagFromImage(ctx context.Context, imageURL string, opts v.log.Debugf("overriding image lookup %s -> %s", imageURL, *override) imageURL = *override } + tagsI, err := v.imageCache.Get(ctx, imageURL, imageURL, nil) if err != nil { return nil, err @@ -58,7 +58,7 @@ func (v *Version) LatestTagFromImage(ctx context.Context, imageURL string, opts // If UseSHA then return early if opts.UseSHA { - tag, err = latestSHA(tags) + tag, err = latestSHA(tags, opts) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func (v *Version) LatestTagFromImage(ctx context.Context, imageURL string, opts } } else { - tag, err = latestSemver(opts, tags) + tag, err = latestSemver(tags, opts) if err != nil { return nil, err } @@ -101,73 +101,3 @@ func (v *Version) Fetch(ctx context.Context, imageURL string, _ *api.Options) (i return tags, nil } - -// latestSemver will return the latest ImageTag based on the given options -// restriction, using semver. This should not be used is UseSHA has been -// enabled. -func latestSemver(opts *api.Options, tags []api.ImageTag) (*api.ImageTag, error) { - var ( - latestImageTag *api.ImageTag - latestV *semver.SemVer - ) - - for i := range tags { - v := semver.Parse(tags[i].Tag) - - // If regex enabled continue here. - // If we match, and is less than, update latest. - if opts.RegexMatcher != nil { - if opts.RegexMatcher.MatchString(tags[i].Tag) && - (latestV == nil || latestV.LessThan(v)) { - latestV = v - latestImageTag = &tags[i] - } - - continue - } - - // If we have declared we wont use metadata but version has it, continue. - if !opts.UseMetaData && v.HasMetaData() { - continue - } - - if opts.PinMajor != nil && *opts.PinMajor != v.Major() { - continue - } - if opts.PinMinor != nil && *opts.PinMinor != v.Minor() { - continue - } - if opts.PinPatch != nil && *opts.PinPatch != v.Patch() { - continue - } - - // If no latest yet set - if latestV == nil || - // If the latest set is less than - latestV.LessThan(v) || - // If the latest is the same tag, but smaller timestamp - (latestV.Equal(v) && tags[i].Timestamp.After(latestImageTag.Timestamp)) { - latestV = v - latestImageTag = &tags[i] - } - } - - if latestImageTag == nil { - return nil, fmt.Errorf("no suitable version found") - } - - return latestImageTag, nil -} - -// latestSHA will return the latest ImageTag based on image timestamps. -func latestSHA(tags []api.ImageTag) (*api.ImageTag, error) { - var latestTag *api.ImageTag - - for i := range tags { - if latestTag == nil || tags[i].Timestamp.After(latestTag.Timestamp) { - latestTag = &tags[i] - } - } - - return latestTag, nil -}