diff --git a/cmd/app/app.go b/cmd/app/app.go index 647e2bc4..beb7f8bc 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -3,10 +3,13 @@ package app import ( "context" "fmt" - "os" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + + "github.com/go-chi/transport" + "github.com/hashicorp/go-cleanhttp" + "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" // Load all auth plugins @@ -35,11 +38,7 @@ func NewCommand(ctx context.Context) *cobra.Command { return fmt.Errorf("failed to parse --log-level %q: %s", opts.LogLevel, err) } - - nlog := logrus.New() - nlog.SetOutput(os.Stdout) - nlog.SetLevel(logLevel) - log := logrus.NewEntry(nlog) + log := newLogger(logLevel) restConfig, err := opts.kubeConfigFlags.ToRESTConfig() if err != nil { @@ -51,18 +50,23 @@ func NewCommand(ctx context.Context) *cobra.Command { return fmt.Errorf("failed to build kubernetes client: %s", err) } - metrics := metrics.New(log) - if err := metrics.Run(opts.MetricsServingAddress); err != nil { + metricsServer := metrics.NewServer(log) + if err := metricsServer.Run(opts.MetricsServingAddress); err != nil { return fmt.Errorf("failed to start metrics server: %s", err) } + opts.Client.Transport = transport.Chain( + cleanhttp.DefaultTransport(), + metricsServer.RoundTripper, + ) + client, err := client.New(ctx, log, opts.Client) if err != nil { return fmt.Errorf("failed to setup image registry clients: %s", err) } defer func() { - if err := metrics.Shutdown(); err != nil { + if err := metricsServer.Shutdown(); err != nil { log.Error(err) } }() @@ -74,7 +78,7 @@ func NewCommand(ctx context.Context) *cobra.Command { log.Infof("flag --test-all-containers=%t %s", opts.DefaultTestAll, defaultTestAllInfoMsg) - c := controller.New(opts.CacheTimeout, metrics, + c := controller.New(opts.CacheTimeout, metricsServer, client, kubeClient, log, opts.DefaultTestAll) return c.Run(ctx, opts.CacheTimeout/2) diff --git a/cmd/app/helpers.go b/cmd/app/helpers.go new file mode 100644 index 00000000..43c44a8a --- /dev/null +++ b/cmd/app/helpers.go @@ -0,0 +1,15 @@ +package app + +import ( + "os" + + "github.com/sirupsen/logrus" +) + +func newLogger(logLevel logrus.Level) *logrus.Entry { + nlog := logrus.New() + nlog.SetOutput(os.Stdout) + nlog.SetLevel(logLevel) + log := logrus.NewEntry(nlog) + return log +} diff --git a/go.mod b/go.mod index 472579a9..4a9802ce 100644 --- a/go.mod +++ b/go.mod @@ -18,33 +18,36 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 - golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect k8s.io/api v0.32.3 k8s.io/apimachinery v0.32.3 k8s.io/cli-runtime v0.32.3 k8s.io/client-go v0.32.3 k8s.io/component-base v0.32.3 - k8s.io/utils v0.0.0-20241210054802-24370beab758 + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e ) require ( github.com/aws/aws-sdk-go-v2/config v1.29.9 github.com/aws/aws-sdk-go-v2/credentials v1.17.62 github.com/aws/aws-sdk-go-v2/service/ecr v1.43.0 + github.com/go-chi/transport v0.5.0 github.com/gofri/go-github-ratelimit v1.1.1 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.3 github.com/google/go-github/v70 v70.0.0 + github.com/hashicorp/go-cleanhttp v0.5.2 github.com/jarcoal/httpmock v1.3.1 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/stretchr/testify v1.10.0 ) require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.2 // indirect + github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect @@ -54,22 +57,22 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect - github.com/aws/smithy-go v1.22.2 // indirect + github.com/aws/smithy-go v1.22.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.16.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v27.5.0+incompatible // indirect + github.com/docker/cli v28.0.2+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/docker/docker-credential-helpers v0.9.3 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -80,11 +83,10 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -95,33 +97,35 @@ 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 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/vbatts/tar-split v0.11.6 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.0 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect - golang.org/x/crypto v0.35.0 // indirect - golang.org/x/net v0.36.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.29.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/time v0.9.0 // indirect - google.golang.org/protobuf v1.36.3 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.1.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/kustomize/api v0.18.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect + sigs.k8s.io/kustomize/api v0.19.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 71115797..9753bf58 100644 --- a/go.sum +++ b/go.sum @@ -9,13 +9,19 @@ github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUd github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= 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/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= +github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= +github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= 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/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= +github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= @@ -44,6 +50,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5 github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= +github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 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= @@ -61,26 +69,38 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.0.2+incompatible h1:cRPZ77FK3/IXTAIQQj1vmhlxiLS5m+MIUDwS6f57lrE= +github.com/docker/cli v28.0.2+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/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-chi/transport v0.5.0 h1:xpnYcIOpBRrduJD68gX9YxkJouRGIE1y+rK5yGYnMXE= +github.com/go-chi/transport v0.5.0/go.mod h1:uoCleTaQiFtoatEiiqcXFZ5OxIp6s1DfGeVsCVbalT4= 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.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofri/go-github-ratelimit v1.1.1 h1:5TCOtFf45M2PjSYU17txqbiYBEzjOuK1+OhivbW69W0= @@ -100,6 +120,7 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -138,6 +159,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.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= @@ -175,6 +198,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -188,8 +215,12 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 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/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= 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= @@ -199,6 +230,7 @@ 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.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -215,6 +247,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= @@ -232,6 +266,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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= @@ -249,19 +285,27 @@ 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.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -273,6 +317,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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= @@ -280,6 +326,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 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= @@ -289,12 +337,17 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= @@ -305,6 +358,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -315,8 +370,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= @@ -331,15 +386,28 @@ 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-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/pkg/api/types.go b/pkg/api/types.go index 59bb44c4..cb2ddfb5 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1,6 +1,7 @@ package api import ( + "context" "regexp" "time" ) @@ -71,3 +72,22 @@ type ImageTag struct { type OS string type Architecture string + +// ImageClient represents a image registry client that can list available tags +// for image URLs. +type ImageClient interface { + // Returns the name of the client + Name() string + + // IsHost will return true if this client is appropriate for the given + // host. + IsHost(host string) bool + + // RepoImage will return the registries repository and image, from a given + // URL path. + RepoImageFromPath(path string) (string, string) + + // Tags will return the available tags for the given host, repo, and image + // using that client. + Tags(ctx context.Context, host, repo, image string) ([]ImageTag, error) +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 8b54fa9b..28a037eb 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -3,6 +3,7 @@ package client import ( "context" "fmt" + "net/http" "strings" "github.com/sirupsen/logrus" @@ -14,34 +15,17 @@ import ( "github.com/jetstack/version-checker/pkg/client/fallback" "github.com/jetstack/version-checker/pkg/client/gcr" "github.com/jetstack/version-checker/pkg/client/ghcr" + "github.com/jetstack/version-checker/pkg/client/oci" "github.com/jetstack/version-checker/pkg/client/quay" "github.com/jetstack/version-checker/pkg/client/selfhosted" ) -// ImageClient represents a image registry client that can list available tags -// for image URLs. -type ImageClient interface { - // Returns the name of the client - Name() string - - // IsHost will return true if this client is appropriate for the given - // host. - IsHost(host string) bool - - // RepoImage will return the registries repository and image, from a given - // URL path. - RepoImageFromPath(path string) (string, string) - - // Tags will return the available tags for the given host, repo, and image - // using that client. - Tags(ctx context.Context, host, repo, image string) ([]api.ImageTag, error) -} - // Client is a container image registry client to list tags of given image // URLs. type Client struct { - clients []ImageClient - fallbackClient ImageClient + clients []api.ImageClient + fallbackClient api.ImageClient + log *logrus.Entry } // Options used to configure client authentication. @@ -52,33 +36,35 @@ type Options struct { GHCR ghcr.Options Docker docker.Options Quay quay.Options + OCI oci.Options Selfhosted map[string]*selfhosted.Options + Transport http.RoundTripper } func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) { acrClient, err := acr.New(opts.ACR) if err != nil { - return nil, fmt.Errorf("failed to create acr client: %s", err) + return nil, fmt.Errorf("failed to create acr client: %w", err) } dockerClient, err := docker.New(ctx, opts.Docker) if err != nil { - return nil, fmt.Errorf("failed to create docker client: %s", err) + return nil, fmt.Errorf("failed to create docker client: %w", err) } - var selfhostedClients []ImageClient + var selfhostedClients []api.ImageClient for _, sOpts := range opts.Selfhosted { sClient, err := selfhosted.New(ctx, log, sOpts) if err != nil { - return nil, fmt.Errorf("failed to create selfhosted client %q: %s", + return nil, fmt.Errorf("failed to create selfhosted client %q: %w", sOpts.Host, err) } selfhostedClients = append(selfhostedClients, sClient) } - fallbackClient, err := fallback.New(ctx, log) + fallbackClient, err := fallback.New(ctx, log, opts.Transport) if err != nil { - return nil, fmt.Errorf("failed to create fallback client: %s", err) + return nil, fmt.Errorf("failed to create fallback client: %w", err) } c := &Client{ @@ -89,13 +75,14 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) dockerClient, gcr.New(opts.GCR), ghcr.New(opts.GHCR), - quay.New(opts.Quay), + quay.New(opts.Quay, log), ), fallbackClient: fallbackClient, + log: log.WithField("client", "registry"), } for _, client := range append(c.clients, fallbackClient) { - log.Debugf("registered client %q", client.Name()) + log.WithField("client", client.Name()).Debugf("registered client") } return c, nil @@ -104,13 +91,16 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) // Tags returns the full list of image tags available, for a given image URL. func (c *Client) Tags(ctx context.Context, imageURL string) ([]api.ImageTag, error) { client, host, path := c.fromImageURL(imageURL) + + c.log.Debugf("using client %q for image URL %q", client.Name(), imageURL) repo, image := client.RepoImageFromPath(path) + return client.Tags(ctx, host, repo, image) } // fromImageURL will return the appropriate registry client for a given // image URL, and the host + path to search. -func (c *Client) fromImageURL(imageURL string) (ImageClient, string, string) { +func (c *Client) fromImageURL(imageURL string) (api.ImageClient, string, string) { var host, path string if strings.Contains(imageURL, ".") || strings.Contains(imageURL, ":") { diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 1de2042a..7e32945f 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -7,6 +7,7 @@ import ( "github.com/sirupsen/logrus" + "github.com/jetstack/version-checker/pkg/api" "github.com/jetstack/version-checker/pkg/client/acr" "github.com/jetstack/version-checker/pkg/client/docker" "github.com/jetstack/version-checker/pkg/client/ecr" @@ -34,7 +35,7 @@ func TestFromImageURL(t *testing.T) { tests := map[string]struct { url string - expClient ImageClient + expClient api.ImageClient expHost string expPath string }{ diff --git a/pkg/client/docker/docker.go b/pkg/client/docker/docker.go index 0efb253e..06eeed13 100644 --- a/pkg/client/docker/docker.go +++ b/pkg/client/docker/docker.go @@ -19,9 +19,10 @@ const ( ) type Options struct { - Username string - Password string - Token string + Username string + Password string + Token string + Transporter http.RoundTripper } type Client struct { @@ -52,7 +53,8 @@ type Image struct { func New(ctx context.Context, opts Options) (*Client, error) { client := &http.Client{ - Timeout: time.Second * 10, + Timeout: time.Second * 10, + Transport: opts.Transporter, } // Setup Auth if username and password used. diff --git a/pkg/client/ecr/ecr.go b/pkg/client/ecr/ecr.go index a7b0305a..3b95701a 100644 --- a/pkg/client/ecr/ecr.go +++ b/pkg/client/ecr/ecr.go @@ -3,6 +3,7 @@ package ecr import ( "context" "fmt" + "net/http" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -24,6 +25,7 @@ type Options struct { AccessKeyID string SecretAccessKey string SessionToken string + Transporter http.RoundTripper } func New(opts Options) *Client { @@ -93,11 +95,15 @@ func (c *Client) createClient(ctx context.Context, region string) (*ecr.Client, if c.IamRoleArn != "" { cfg, err = config.LoadDefaultConfig(ctx, config.WithRegion(region), + config.WithHTTPClient(&http.Client{Transport: c.Options.Transporter}), ) } else { cfg, err = config.LoadDefaultConfig(ctx, - config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(c.AccessKeyID, c.SecretAccessKey, c.SessionToken)), + config.WithCredentialsProvider( + credentials.NewStaticCredentialsProvider(c.AccessKeyID, c.SecretAccessKey, c.SessionToken), + ), config.WithRegion(region), + config.WithHTTPClient(&http.Client{Transport: c.Options.Transporter}), ) } if err != nil { diff --git a/pkg/client/fallback/fallback.go b/pkg/client/fallback/fallback.go index f41e7998..502025bf 100644 --- a/pkg/client/fallback/fallback.go +++ b/pkg/client/fallback/fallback.go @@ -2,24 +2,32 @@ package fallback import ( "context" + "fmt" + "net/http" + "time" "github.com/jetstack/version-checker/pkg/api" "github.com/jetstack/version-checker/pkg/client/oci" "github.com/jetstack/version-checker/pkg/client/selfhosted" + + "github.com/patrickmn/go-cache" + "github.com/sirupsen/logrus" ) type Client struct { SelfHosted *selfhosted.Client OCI *oci.Client + log *logrus.Entry + hostCache *cache.Cache } -func New(ctx context.Context, log *logrus.Entry) (*Client, error) { - sh, err := selfhosted.New(ctx, log, new(selfhosted.Options)) +func New(ctx context.Context, log *logrus.Entry, transporter http.RoundTripper) (*Client, error) { + sh, err := selfhosted.New(ctx, log, &selfhosted.Options{Transporter: transporter}) if err != nil { return nil, err } - oci, err := oci.New() + oci, err := oci.New(&oci.Options{Transporter: transporter}) if err != nil { return nil, err } @@ -27,6 +35,8 @@ func New(ctx context.Context, log *logrus.Entry) (*Client, error) { return &Client{ SelfHosted: sh, OCI: oci, + hostCache: cache.New(5*time.Hour, 10*time.Hour), + log: log.WithField("client", "fallback"), }, nil } @@ -34,12 +44,35 @@ func (c *Client) Name() string { return "fallback" } -func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.ImageTag, error) { - // TODO: Cache selfhosted/oci by host +func (c *Client) Tags(ctx context.Context, host, repo, image string) (tags []api.ImageTag, err error) { + // Check if we have a cached client for the host + if client, found := c.hostCache.Get(host); found { + c.log.Infof("Found client for host %s in cache", host) + if client, ok := client.(api.ImageClient); ok { + if tags, err := client.Tags(ctx, host, repo, image); err == nil { + return tags, nil + } + } else { + c.log.Errorf("Unable to fetch from cache for host %s...", host) + } + } + c.log.Debugf("no client for host %s in cache, continuing fallback", host) + + // Try selfhosted client first if tags, err := c.SelfHosted.Tags(ctx, host, repo, image); err == nil { - return tags, err + c.hostCache.SetDefault(host, c.SelfHosted) + return tags, nil } - return c.OCI.Tags(ctx, host, repo, image) + c.log.Debug("failed to lookup via SelfHosted, looking up via OCI") + + // Fallback to OCI client + if tags, err := c.OCI.Tags(ctx, host, repo, image); err == nil { + c.hostCache.SetDefault(host, c.OCI) + return tags, nil + } + + // If both clients fail, return an error + return nil, fmt.Errorf("failed to fetch tags for host: %s, repo: %s, image: %s", host, repo, image) } func (c *Client) IsHost(_ string) bool { diff --git a/pkg/client/oci/oci.go b/pkg/client/oci/oci.go index 9b0edaa1..f27fa1b9 100644 --- a/pkg/client/oci/oci.go +++ b/pkg/client/oci/oci.go @@ -3,6 +3,7 @@ package oci import ( "context" "fmt" + "net/http" "strings" "github.com/google/go-containerregistry/pkg/name" @@ -10,20 +11,43 @@ import ( "github.com/jetstack/version-checker/pkg/api" ) +type CredentialsMode int + +const ( + Auto CredentialsMode = iota + Multi + Single + Manual +) + +type Options struct { + // CredentailsMode CredentialsMode + // ServiceAccountName string + // ServiceAccountNamespace string + Transporter http.RoundTripper +} + // Client is a client for a registry compatible with the OCI Distribution Spec type Client struct { + *Options puller *remote.Puller } // New returns a new client -func New() (*Client, error) { - puller, err := remote.NewPuller() +func New(opts *Options) (*Client, error) { + pullOpts := []remote.Option{} + if opts.Transporter != nil { + pullOpts = append(pullOpts, remote.WithTransport(opts.Transporter)) + } + + puller, err := remote.NewPuller(pullOpts...) if err != nil { return nil, fmt.Errorf("creating puller: %w", err) } return &Client{ - puller: puller, + puller: puller, + Options: opts, }, nil } diff --git a/pkg/client/oci/oci_test.go b/pkg/client/oci/oci_test.go index 10f1443d..f21a2e17 100644 --- a/pkg/client/oci/oci_test.go +++ b/pkg/client/oci/oci_test.go @@ -129,7 +129,7 @@ func TestClientTags(t *testing.T) { t.Run(testName, func(t *testing.T) { host := setupRegistry(t) - c, err := New() + c, err := New(new(Options)) if err != nil { t.Fatalf("unexpected error creating client: %s", err) } @@ -146,7 +146,6 @@ func TestClientTags(t *testing.T) { if diff := cmp.Diff(tc.wantTags, gotTags); diff != "" { t.Errorf("unexpected tags:\n%s", diff) } - }) } } @@ -178,7 +177,7 @@ func TestClientRepoImageFromPath(t *testing.T) { }, } - c, err := New() + c, err := New(new(Options)) if err != nil { t.Fatalf("unexpected error creating client: %s", err) } diff --git a/pkg/client/quay/quay.go b/pkg/client/quay/quay.go index 8b2939f8..4e3c3145 100644 --- a/pkg/client/quay/quay.go +++ b/pkg/client/quay/quay.go @@ -8,6 +8,7 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" + "github.com/sirupsen/logrus" "github.com/jetstack/version-checker/pkg/api" "github.com/jetstack/version-checker/pkg/client/util" @@ -57,10 +58,10 @@ type responseManifestDataItem struct { } `json:"platform"` } -func New(opts Options) *Client { +func New(opts Options, log *logrus.Entry) *Client { client := retryablehttp.NewClient() client.RetryMax = 10 - client.Logger = nil + client.Logger = log.WithField("client", "quay") return &Client{ Options: opts, diff --git a/pkg/client/selfhosted/selfhosted.go b/pkg/client/selfhosted/selfhosted.go index 0d0affa7..e2c25415 100644 --- a/pkg/client/selfhosted/selfhosted.go +++ b/pkg/client/selfhosted/selfhosted.go @@ -16,6 +16,9 @@ import ( "github.com/sirupsen/logrus" + "github.com/go-chi/transport" + "github.com/hashicorp/go-cleanhttp" + "github.com/jetstack/version-checker/pkg/api" selfhostederrors "github.com/jetstack/version-checker/pkg/client/selfhosted/errors" "github.com/jetstack/version-checker/pkg/client/util" @@ -35,13 +38,14 @@ const ( ) type Options struct { - Host string - Username string - Password string - Bearer string - TokenPath string - Insecure bool - CAPath string + Host string + Username string + Password string + Bearer string + TokenPath string + Insecure bool + CAPath string + Transporter http.RoundTripper } type Client struct { @@ -79,17 +83,18 @@ type V1Compatibility struct { func New(ctx context.Context, log *logrus.Entry, opts *Options) (*Client, error) { client := &Client{ Client: &http.Client{ - Timeout: time.Second * 10, + Timeout: time.Second * 10, + Transport: cleanhttp.DefaultTransport(), }, Options: opts, - log: log.WithField("client", opts.Host), + log: log.WithField("client", "selfhosted-"+opts.Host), } if err := configureHost(ctx, client, opts); err != nil { return nil, err } - if err := configureTLS(client, opts); err != nil { + if err := configureTransport(client, opts); err != nil { return nil, err } @@ -142,30 +147,31 @@ func configureAuth(ctx context.Context, client *Client, opts *Options) error { return nil } -func configureTLS(client *Client, opts *Options) error { +func configureTransport(client *Client, opts *Options) error { if client.httpScheme == "" { client.httpScheme = "https" } + baseTransport := cleanhttp.DefaultTransport() + baseTransport.Proxy = http.ProxyFromEnvironment if client.httpScheme == "https" { tlsConfig, err := newTLSConfig(opts.Insecure, opts.CAPath) if err != nil { return err } - - client.Client.Transport = &http.Transport{ - TLSClientConfig: tlsConfig, - Proxy: http.ProxyFromEnvironment, - } + baseTransport.TLSClientConfig = tlsConfig } + client.Transport = transport.Chain(baseTransport, + transport.If(logrus.IsLevelEnabled(logrus.DebugLevel), transport.LogRequests(transport.LogOptions{Concise: true})), + transport.If(opts.Transporter != nil, func(rt http.RoundTripper) http.RoundTripper { return opts.Transporter })) return nil } // Name returns the name of the host URL for the selfhosted client func (c *Client) Name() string { if len(c.Host) == 0 { - return "dockerapi" + return "selfhosted" } return c.Host @@ -330,7 +336,8 @@ func newTLSConfig(insecure bool, CAPath string) (*tls.Config, error) { } return &tls.Config{ - InsecureSkipVerify: insecure, + Renegotiation: tls.RenegotiateOnceAsClient, + InsecureSkipVerify: insecure, // #nosec G402 RootCAs: rootCAs, }, nil } diff --git a/pkg/client/selfhosted/selfhosted_test.go b/pkg/client/selfhosted/selfhosted_test.go index e2ff7149..7c3b762f 100644 --- a/pkg/client/selfhosted/selfhosted_test.go +++ b/pkg/client/selfhosted/selfhosted_test.go @@ -106,7 +106,7 @@ func TestName(t *testing.T) { assert.Equal(t, "testhost", client.Name()) client.Options.Host = "" - assert.Equal(t, "dockerapi", client.Name()) + assert.Equal(t, "selfhosted", client.Name()) } func TestTags(t *testing.T) { diff --git a/pkg/controller/checker/checker.go b/pkg/controller/checker/checker.go index 8149dcce..aab44ad2 100644 --- a/pkg/controller/checker/checker.go +++ b/pkg/controller/checker/checker.go @@ -31,8 +31,12 @@ func New(search search.Searcher) *Checker { } // Container will return the result of the given container's current version, compared to the latest upstream. -func (c *Checker) Container(ctx context.Context, log *logrus.Entry, pod *corev1.Pod, - container *corev1.Container, opts *api.Options) (*Result, error) { +func (c *Checker) Container(ctx context.Context, log *logrus.Entry, + pod *corev1.Pod, + container *corev1.Container, + opts *api.Options, +) (*Result, error) { + statusSHA := containerStatusImageSHA(pod, container.Name) if len(statusSHA) == 0 { return nil, nil @@ -105,39 +109,8 @@ func (c *Checker) handleSemver(ctx context.Context, imageURL, statusSHA, current }, nil } -// containerStatusImageSHA will return the containers image SHA, if it is ready. -func containerStatusImageSHA(pod *corev1.Pod, containerName string) string { - for _, status := range pod.Status.InitContainerStatuses { - if status.Name == containerName { - statusImage, _, statusSHA := urlTagSHAFromImage(status.ImageID) - - // If the image ID contains a URL, use the parsed SHA - if len(statusSHA) > 0 { - return statusSHA - } - - return statusImage - } - } - - // Get the SHA of the current image - for _, status := range pod.Status.ContainerStatuses { - if status.Name == containerName { - statusImage, _, statusSHA := urlTagSHAFromImage(status.ImageID) - - // If the image ID contains a URL, use the parsed SHA - if len(statusSHA) > 0 { - return statusSHA - } - - return statusImage - } - } - - return "" -} -// isLatestOrEmptyTag will return true if the given tag is ” or 'latest'. +// isLatestOrEmptyTag will return true if the given tag is "" or "latest". func (c *Checker) isLatestOrEmptyTag(tag string) bool { return tag == "" || tag == "latest" } @@ -192,39 +165,3 @@ func (c *Checker) isLatestSHA(ctx context.Context, imageURL, currentSHA string, func (c *Checker) Search() search.Searcher { return c.search } - -// urlTagSHAFromImage from will return the image URL, and the semver version -// and or SHA tag. -func urlTagSHAFromImage(image string) (url, version, sha string) { - // If using SHA tag - if split := strings.SplitN(image, "@", 2); len(split) > 1 { - url = split[0] - sha = split[1] - - // Check is url contains version, but also handle ports - firstSlashIndex := strings.Index(split[0], "/") - if firstSlashIndex == -1 { - firstSlashIndex = 0 - } - - // url contains version - if strings.LastIndex(split[0][firstSlashIndex:], ":") > -1 { - lastColonIndex := strings.LastIndex(split[0], ":") - url = split[0][:lastColonIndex] - version = split[0][lastColonIndex+1:] - } - - return - } - - lastColonIndex := strings.LastIndex(image, ":") - if lastColonIndex == -1 { - return image, "", "" - } - - if strings.LastIndex(image, "/") > lastColonIndex { - return image, "", "" - } - - return image[:lastColonIndex], image[lastColonIndex+1:], "" -} diff --git a/pkg/controller/checker/helpers.go b/pkg/controller/checker/helpers.go new file mode 100644 index 00000000..bdab9750 --- /dev/null +++ b/pkg/controller/checker/helpers.go @@ -0,0 +1,94 @@ +package checker + +import ( + "encoding/json" + "fmt" + "hash/fnv" + "strings" + + "github.com/jetstack/version-checker/pkg/api" + corev1 "k8s.io/api/core/v1" +) + +// calculateHashIndex returns a hash index given an imageURL and options. +func calculateHashIndex(imageURL string, opts *api.Options) (string, error) { + optsJSON, err := json.Marshal(opts) + if err != nil { + return "", fmt.Errorf("failed to marshal options: %s", err) + } + + hash := fnv.New32() + 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 +} + +// containerStatusImageSHA will return the containers image SHA, if it is ready. +func containerStatusImageSHA(pod *corev1.Pod, containerName string) string { + for _, status := range pod.Status.InitContainerStatuses { + if status.Name == containerName { + statusImage, _, statusSHA := urlTagSHAFromImage(status.ImageID) + + // If the image ID contains a URL, use the parsed SHA + if len(statusSHA) > 0 { + return statusSHA + } + + return statusImage + } + } + + // Get the SHA of the current image + for _, status := range pod.Status.ContainerStatuses { + if status.Name == containerName { + statusImage, _, statusSHA := urlTagSHAFromImage(status.ImageID) + + // If the image ID contains a URL, use the parsed SHA + if len(statusSHA) > 0 { + return statusSHA + } + + return statusImage + } + } + + return "" +} + +// urlTagSHAFromImage from will return the image URL, and the semver version +// and or SHA tag. +func urlTagSHAFromImage(image string) (url, version, sha string) { + // If using SHA tag + if split := strings.SplitN(image, "@", 2); len(split) > 1 { + url = split[0] + sha = split[1] + + // Check is url contains version, but also handle ports + firstSlashIndex := strings.Index(split[0], "/") + if firstSlashIndex == -1 { + firstSlashIndex = 0 + } + + // url contains version + if strings.LastIndex(split[0][firstSlashIndex:], ":") > -1 { + lastColonIndex := strings.LastIndex(split[0], ":") + url = split[0][:lastColonIndex] + version = split[0][lastColonIndex+1:] + } + + return + } + + lastColonIndex := strings.LastIndex(image, ":") + if lastColonIndex == -1 { + return image, "", "" + } + + if strings.LastIndex(image, "/") > lastColonIndex { + return image, "", "" + } + + return image[:lastColonIndex], image[lastColonIndex+1:], "" +} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 89917c93..2705bed0 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -149,8 +149,9 @@ func (c *Controller) deleteObject(obj interface{}) { } for _, container := range pod.Spec.Containers { - c.log.Debugf("removing deleted pod containers from metrics: %s/%s/%s", - pod.Namespace, pod.Name, container.Name) + c.log.WithFields( + logrus.Fields{"pod": pod.Name, "container": container.Name, "namespace": pod.Namespace}, + ).Debug("removing deleted pod containers from metrics") c.metrics.RemoveImage(pod.Namespace, pod.Name, container.Name, "init") c.metrics.RemoveImage(pod.Namespace, pod.Name, container.Name, "container") } diff --git a/pkg/controller/sync.go b/pkg/controller/sync.go index d422acc6..27b8f258 100644 --- a/pkg/controller/sync.go +++ b/pkg/controller/sync.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -40,8 +41,13 @@ func (c *Controller) sync(ctx context.Context, pod *corev1.Pod) error { } // 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 { +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) { c.metrics.RemoveImage(pod.Namespace, pod.Name, container.Name, containerType) @@ -73,8 +79,18 @@ func (c *Controller) syncContainer(ctx context.Context, log *logrus.Entry, build // checkContainer will check the given container and options, and update // metrics according to the result. -func (c *Controller) checkContainer(ctx context.Context, log *logrus.Entry, pod *corev1.Pod, - container *corev1.Container, containerType string, opts *api.Options) error { +func (c *Controller) checkContainer(ctx context.Context, log *logrus.Entry, + pod *corev1.Pod, + container *corev1.Container, + containerType string, + opts *api.Options, +) error { + + startTime := time.Now() + defer func() { + c.metrics.RegisterImageDuration(pod.Namespace, pod.Name, container.Name, container.Image, startTime) + }() + result, err := c.checker.Container(ctx, log, pod, container, opts) if err != nil { return err diff --git a/pkg/controller/sync_test.go b/pkg/controller/sync_test.go index ba1ade4e..a7fa876d 100644 --- a/pkg/controller/sync_test.go +++ b/pkg/controller/sync_test.go @@ -21,8 +21,9 @@ import ( // Test for the sync method. func TestController_Sync(t *testing.T) { + t.Parallel() log := logrus.NewEntry(logrus.New()) - metrics := &metrics.Metrics{} + metrics := metrics.NewServer(log) imageClient := &client.Client{} searcher := search.New(log, 5*time.Minute, version.New(log, imageClient, 5*time.Minute)) checker := checker.New(searcher) @@ -55,8 +56,9 @@ func TestController_Sync(t *testing.T) { // Test for the syncContainer method. func TestController_SyncContainer(t *testing.T) { + t.Parallel() log := logrus.NewEntry(logrus.New()) - metrics := &metrics.Metrics{} + metrics := metrics.NewServer(log) imageClient := &client.Client{} searcher := search.New(log, 5*time.Minute, version.New(log, imageClient, 5*time.Minute)) checker := checker.New(searcher) @@ -86,8 +88,9 @@ func TestController_SyncContainer(t *testing.T) { // Test for the checkContainer method. func TestController_CheckContainer(t *testing.T) { + t.Parallel() log := logrus.NewEntry(logrus.New()) - metrics := &metrics.Metrics{} + metrics := metrics.NewServer(log) imageClient := &client.Client{} searcher := search.New(log, 5*time.Minute, version.New(log, imageClient, 5*time.Minute)) checker := checker.New(searcher) @@ -114,8 +117,10 @@ func TestController_CheckContainer(t *testing.T) { // Example of testing syncContainer when version is not found. func TestController_SyncContainer_NoVersionFound(t *testing.T) { + t.Parallel() + log := logrus.NewEntry(logrus.New()) - metrics := &metrics.Metrics{} + metrics := metrics.NewServer(log) imageClient := &client.Client{} searcher := search.New(log, 5*time.Minute, version.New(log, imageClient, 5*time.Minute)) checker := checker.New(searcher) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 090baed6..413d8f60 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -20,9 +20,14 @@ import ( // metrics. type Metrics struct { *http.Server + log *logrus.Entry - containerImageVersion *prometheus.GaugeVec - log *logrus.Entry + registry *prometheus.Registry + containerImageVersion *prometheus.GaugeVec + containerImageDuration *prometheus.GaugeVec + + // Contains all metrics for the roundtripper + roundTripper *RoundTripper // container cache stores a cache of a container's current image, version, // and the latest @@ -36,8 +41,10 @@ type cacheItem struct { latestVersion string } -func New(log *logrus.Entry) *Metrics { - containerImageVersion := promauto.NewGaugeVec( +func NewServer(log *logrus.Entry) *Metrics { + // Reset the prometheus registry + reg := prometheus.NewRegistry() + containerImageVersion := promauto.With(reg).NewGaugeVec( prometheus.GaugeOpts{ Namespace: "version_checker", Name: "is_latest_version", @@ -47,18 +54,29 @@ func New(log *logrus.Entry) *Metrics { "namespace", "pod", "container", "container_type", "image", "current_version", "latest_version", }, ) + containerImageDuration := promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "version_checker", + Name: "image_lookup_duration", + Help: "Time taken to lookup version.", + }, + []string{"namespace", "pod", "container", "image"}, + ) return &Metrics{ - log: log.WithField("module", "metrics"), - containerImageVersion: containerImageVersion, - containerCache: make(map[string]cacheItem), + log: log.WithField("module", "metrics"), + registry: reg, + containerImageVersion: containerImageVersion, + containerImageDuration: containerImageDuration, + containerCache: make(map[string]cacheItem), + roundTripper: NewRoundTripper(reg), } } // Run will run the metrics server. func (m *Metrics) Run(servingAddress string) error { router := http.NewServeMux() - router.Handle("/metrics", promhttp.Handler()) + router.Handle("/metrics", promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{})) router.Handle("/healthz", http.HandlerFunc(m.healthzAndReadyzHandler)) router.Handle("/readyz", http.HandlerFunc(m.healthzAndReadyzHandler)) @@ -124,9 +142,20 @@ func (m *Metrics) RemoveImage(namespace, pod, container, containerType string) { m.containerImageVersion.DeletePartialMatch( m.buildPartialLabels(namespace, pod), ) + m.containerImageDuration.DeletePartialMatch( + m.buildPartialLabels(namespace, pod), + ) delete(m.containerCache, index) } +func (m *Metrics) RegisterImageDuration(namespace, pod, container, image string, startTime time.Time) { + m.mu.Lock() + defer m.mu.Unlock() + + m.containerImageDuration.WithLabelValues(namespace, pod, container, image). + Set(time.Since(startTime).Seconds()) +} + func (m *Metrics) latestImageIndex(namespace, pod, container, containerType string) string { return strings.Join([]string{namespace, pod, container, containerType}, "") } diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 518d1780..2ad67b59 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -9,7 +9,7 @@ import ( ) func TestCache(t *testing.T) { - m := New(logrus.NewEntry(logrus.New())) + m := NewServer(logrus.NewEntry(logrus.New())) for i, typ := range []string{"init", "container"} { version := fmt.Sprintf("0.1.%d", i) diff --git a/pkg/metrics/roundtripper.go b/pkg/metrics/roundtripper.go new file mode 100644 index 00000000..152e4951 --- /dev/null +++ b/pkg/metrics/roundtripper.go @@ -0,0 +1,150 @@ +package metrics + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/httptrace" + "net/url" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type RoundTripper struct { + base http.RoundTripper + + clientInFlightGauge prometheus.Gauge + clientCounter *prometheus.CounterVec + histVec *prometheus.GaugeVec + tlsLatencyVec *prometheus.GaugeVec + dnsLatencyVec *prometheus.GaugeVec +} + +// RoundTripper provides Prometheus instrumentation for an HTTP client, including domain labels. +func (m *Metrics) RoundTripper(baseTransport http.RoundTripper) http.RoundTripper { + if baseTransport == nil { + baseTransport = http.DefaultTransport + } + + if m.roundTripper == nil { + m.roundTripper = NewRoundTripper(m.registry) + } + m.roundTripper.base = baseTransport + + return promhttp.InstrumentRoundTripperInFlight(m.roundTripper.clientInFlightGauge, + m.roundTripper, + ) +} + +func NewRoundTripper(reg prometheus.Registerer) *RoundTripper { + return &RoundTripper{ + clientInFlightGauge: promauto.With(reg).NewGauge( + prometheus.GaugeOpts{ + Name: "client_in_flight_requests", + Help: "A gauge of in-flight requests for the wrapped client.", + Namespace: "http", + }), + + clientCounter: promauto.With(reg).NewCounterVec( + prometheus.CounterOpts{ + Name: "client_requests_total", + Help: "A counter for requests from the wrapped client.", + Namespace: "http", + }, + []string{"code", "method", "domain"}, // Ensure domain is explicitly part of the label definition + ), + + histVec: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "client_request_duration_seconds", + Help: "A histogram of request durations.", + Namespace: "http", + }, + []string{"method", "domain"}, // Explicit labels + ), + + tlsLatencyVec: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "tls_duration_seconds", + Help: "Trace TLS latency histogram.", + Namespace: "http", + }, + []string{"event", "domain"}, + ), + + dnsLatencyVec: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "dns_duration_seconds", + Help: "Trace DNS latency histogram.", + Namespace: "http", + }, + []string{"event", "domain"}, + ), + } +} + +// extractDomain extracts the domain (TLD) from the request URL. +func extractDomain(req *http.Request) string { + if req.URL == nil { + return "unknown" + } + parsedURL, err := url.Parse(req.URL.String()) + if err != nil { + return "unknown" + } + host := parsedURL.Hostname() + if strings.Contains(host, ":") { + host, _, _ = net.SplitHostPort(host) + } + return host +} + +func (t *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + domain := extractDomain(req) + + // Track request duration + startTime := time.Now() + + // Track DNS and TLS latencies + var dnsStart, dnsEnd, tlsStart, tlsEnd time.Time + + trace := &httptrace.ClientTrace{ + DNSStart: func(_ httptrace.DNSStartInfo) { + dnsStart = time.Now() + }, + DNSDone: func(_ httptrace.DNSDoneInfo) { + dnsEnd = time.Now() + t.dnsLatencyVec.WithLabelValues("dns_done", domain).Set(dnsEnd.Sub(dnsStart).Seconds()) + }, + TLSHandshakeStart: func() { + tlsStart = time.Now() + }, + TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { + tlsEnd = time.Now() + t.tlsLatencyVec.WithLabelValues("tls_done", domain).Set(tlsEnd.Sub(tlsStart).Seconds()) + }, + } + + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + + // Perform the request + resp, err := t.base.RoundTrip(req) + + // Manually record request duration + t.histVec.WithLabelValues(req.Method, domain).Set(time.Since(startTime).Seconds()) + + if err != nil { + // In case of failure, still increment counter + t.clientCounter.WithLabelValues("error", req.Method, domain).Inc() + return nil, err + } + + // Increment counter with domain label + t.clientCounter.WithLabelValues(http.StatusText(resp.StatusCode), req.Method, domain).Inc() + + return resp, nil +} diff --git a/pkg/metrics/roundtripper_test.go b/pkg/metrics/roundtripper_test.go new file mode 100644 index 00000000..19c153ba --- /dev/null +++ b/pkg/metrics/roundtripper_test.go @@ -0,0 +1,199 @@ +package metrics + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/go-chi/transport" + "github.com/sirupsen/logrus" + + "github.com/prometheus/client_golang/prometheus/testutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var log = logrus.NewEntry(logrus.StandardLogger()) + +func TestExtractDomain(t *testing.T) { + tests := []struct { + name string + req *http.Request + expected string + }{ + { + name: "valid URL with domain", + req: &http.Request{ + URL: &url.URL{ + Scheme: "https", + Host: "example.com", + }, + }, + expected: "example.com", + }, + { + name: "valid URL with subdomain", + req: &http.Request{ + URL: &url.URL{ + Scheme: "https", + Host: "sub.example.com", + }, + }, + expected: "sub.example.com", + }, + { + name: "URL with port", + req: &http.Request{ + URL: &url.URL{ + Scheme: "https", + Host: "example.com:8080", + }, + }, + expected: "example.com", + }, + { + name: "nil URL", + req: &http.Request{}, + expected: "unknown", + }, + { + name: "URL with port", + req: &http.Request{ + URL: &url.URL{ + Scheme: "https", + Host: "with-port:8443", + }, + }, + expected: "with-port", + }, + { + name: "invalid URL", + req: &http.Request{ + URL: &url.URL{ + Scheme: "https", + Host: "invalid-url", + }, + }, + expected: "invalid-url", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + domain := extractDomain(tt.req) + assert.Equal(t, tt.expected, domain) + }) + } +} + +func TestRoundTripper(t *testing.T) { + // t.Skipf("Still need to fix these") + t.Parallel() + + tests := []struct { + name string + handler http.HandlerFunc + expectedStatus int + expectedError bool + expectedMetricString string + }{ + { + name: "successful request", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }, + expectedStatus: http.StatusOK, + expectedError: false, + expectedMetricString: ` + # HELP http_client_in_flight_requests A gauge of in-flight requests for the wrapped client. + # TYPE http_client_in_flight_requests gauge + http_client_in_flight_requests 0 + # HELP http_client_request_duration_seconds A histogram of request durations. + # TYPE http_client_request_duration_seconds gauge + http_client_request_duration_seconds{domain="127.0.0.1",method="GET"} 0 + # HELP http_client_requests_total A counter for requests from the wrapped client. + # TYPE http_client_requests_total counter + http_client_requests_total{code="OK",domain="127.0.0.1",method="GET"} 1 + `, + }, + { + name: "failed request", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + expectedStatus: http.StatusInternalServerError, + expectedError: false, + expectedMetricString: ` + # HELP http_client_in_flight_requests A gauge of in-flight requests for the wrapped client. + # TYPE http_client_in_flight_requests gauge + http_client_in_flight_requests 0 + # HELP http_client_request_duration_seconds A histogram of request durations. + # TYPE http_client_request_duration_seconds gauge + http_client_request_duration_seconds{domain="127.0.0.1",method="GET"} 0 + # HELP http_client_requests_total A counter for requests from the wrapped client. + # TYPE http_client_requests_total counter + http_client_requests_total{code="Internal Server Error",domain="127.0.0.1",method="GET"} 1 + `, + }, + { + name: "request with DNS and TLS latency", + handler: func(w http.ResponseWriter, r *http.Request) { + time.Sleep(10 * time.Millisecond) + w.WriteHeader(http.StatusOK) + }, + expectedStatus: http.StatusOK, + expectedError: false, + expectedMetricString: ` + # HELP http_client_in_flight_requests A gauge of in-flight requests for the wrapped client. + # TYPE http_client_in_flight_requests gauge + http_client_in_flight_requests 0 + # HELP http_client_request_duration_seconds A histogram of request durations. + # TYPE http_client_request_duration_seconds gauge + http_client_request_duration_seconds{domain="127.0.0.1",method="GET"} 0 + # HELP http_client_requests_total A counter for requests from the wrapped client. + # TYPE http_client_requests_total counter + http_client_requests_total{code="OK",domain="127.0.0.1",method="GET"} 1 +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + metricsServer := NewServer(log) + server := httptest.NewServer(tt.handler) + defer server.Close() + + client := &http.Client{ + Transport: transport.Chain(http.DefaultTransport, metricsServer.RoundTripper), + } + + req, err := http.NewRequest("GET", server.URL, nil) + require.NoError(t, err) + + resp, err := client.Do(req) + if tt.expectedError { + assert.Error(t, err) + assert.Nil(t, resp) + } else { + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, tt.expectedStatus, resp.StatusCode) + } + + // Validate metrics + assert.NoError(t, + testutil.GatherAndCompare( + metricsServer.registry, strings.NewReader(tt.expectedMetricString), + "http_client_in_flight_requests", + "http_client_requests_total", + // "http_client_request_duration_seconds", + "http_tls_duration_seconds", + "http_dns_duration_seconds", + )) + }) + } +}