From 919d28f3d60102850c8bdd49980c495a7ac89ceb Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 16:07:19 +0000 Subject: [PATCH 1/9] Adding http metrics to calculate upstream response times --- cmd/app/app.go | 13 ++- pkg/client/client.go | 16 +++- pkg/client/docker/docker.go | 10 ++- pkg/client/ecr/ecr.go | 8 +- pkg/client/fallback/fallback.go | 42 +++++++-- pkg/client/oci/oci.go | 27 +++++- pkg/client/oci/oci_test.go | 5 +- pkg/client/quay/quay.go | 5 +- pkg/client/selfhosted/selfhosted.go | 40 +++++---- pkg/controller/checker/checker.go | 77 ++-------------- pkg/controller/checker/helpers.go | 94 ++++++++++++++++++++ pkg/controller/controller.go | 5 +- pkg/controller/sync.go | 23 ++++- pkg/controller/sync_test.go | 12 ++- pkg/metrics/metrics.go | 33 +++++-- pkg/metrics/metrics_test.go | 2 +- pkg/metrics/roundtripper.go | 131 ++++++++++++++++++++++++++++ 17 files changed, 410 insertions(+), 133 deletions(-) create mode 100644 pkg/controller/checker/helpers.go create mode 100644 pkg/metrics/roundtripper.go diff --git a/cmd/app/app.go b/cmd/app/app.go index 647e2bc4..d680960a 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -51,18 +51,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( + http.DefaultTransport, + metrics.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 +79,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/pkg/client/client.go b/pkg/client/client.go index 8b54fa9b..b2f1ebae 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,6 +15,7 @@ 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" ) @@ -52,7 +54,9 @@ 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) { @@ -65,7 +69,7 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) return nil, fmt.Errorf("failed to create docker client: %s", err) } - var selfhostedClients []ImageClient + var selfhostedClients []api.ImageClient for _, sOpts := range opts.Selfhosted { sClient, err := selfhosted.New(ctx, log, sOpts) if err != nil { @@ -76,7 +80,7 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) 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) } @@ -89,13 +93,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,7 +109,10 @@ 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) } diff --git a/pkg/client/docker/docker.go b/pkg/client/docker/docker.go index 78e70d2c..63f2c7ae 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..a2326baf 100644 --- a/pkg/client/fallback/fallback.go +++ b/pkg/client/fallback/fallback.go @@ -2,24 +2,31 @@ package fallback import ( "context" + "fmt" + "net/http" "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 +34,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 +43,31 @@ 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 tags, err := client.Tags(ctx, host, repo, image); err == nil { + return tags, nil + } + } + 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..939bc8d4 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,40 @@ 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) { + puller, err := remote.NewPuller( + remote.WithTransport(opts.Transporter), + ) 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 b8361919..6488458f 100644 --- a/pkg/client/selfhosted/selfhosted.go +++ b/pkg/client/selfhosted/selfhosted.go @@ -35,13 +35,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 +80,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 +144,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 +333,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/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..273b8674 100644 --- a/pkg/controller/sync.go +++ b/pkg/controller/sync.go @@ -40,8 +40,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 +78,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..3ff59f03 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,9 @@ 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..acdf45fc 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -21,8 +21,9 @@ import ( type Metrics struct { *http.Server - containerImageVersion *prometheus.GaugeVec - log *logrus.Entry + containerImageVersion *prometheus.GaugeVec + containerImageDuration *prometheus.GaugeVec + log *logrus.Entry // container cache stores a cache of a container's current image, version, // and the latest @@ -36,7 +37,7 @@ type cacheItem struct { latestVersion string } -func New(log *logrus.Entry) *Metrics { +func NewServer(log *logrus.Entry) *Metrics { containerImageVersion := promauto.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "version_checker", @@ -47,11 +48,20 @@ func New(log *logrus.Entry) *Metrics { "namespace", "pod", "container", "container_type", "image", "current_version", "latest_version", }, ) + containerImageDuration := promauto.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"), + containerImageVersion: containerImageVersion, + containerImageDuration: containerImageDuration, + containerCache: make(map[string]cacheItem), } } @@ -124,9 +134,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..de6642a3 --- /dev/null +++ b/pkg/metrics/roundtripper.go @@ -0,0 +1,131 @@ +package metrics + +import ( + "crypto/tls" + "net/http" + "net/http/httptrace" + "net/url" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + clientInFlightGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "client_in_flight_requests", + Help: "A gauge of in-flight requests for the wrapped client.", + Namespace: "http", + }) + + clientCounter = promauto.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.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "client_request_duration_seconds", + Help: "A histogram of request durations.", + Namespace: "http", + }, + []string{"method", "domain"}, // Explicit labels + ) + + tlsLatencyVec = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "tls_duration_seconds", + Help: "Trace TLS latency histogram.", + Namespace: "http", + }, + []string{"event", "domain"}, + ) + + dnsLatencyVec = promauto.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" + } + return parsedURL.Hostname() +} + +// tracingRoundTripper wraps RoundTripper to track request metrics. +type tracingRoundTripper struct { + base http.RoundTripper +} + +func (t *tracingRoundTripper) 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() + dnsLatencyVec.WithLabelValues("dns_done", domain).Set(dnsEnd.Sub(dnsStart).Seconds()) + }, + TLSHandshakeStart: func() { + tlsStart = time.Now() + }, + TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { + tlsEnd = time.Now() + 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 + histVec.WithLabelValues(req.Method, domain).Set(time.Since(startTime).Seconds()) + + if err != nil { + // In case of failure, still increment counter + clientCounter.WithLabelValues("error", req.Method, domain).Inc() + return nil, err + } + + // Increment counter with domain label + clientCounter.WithLabelValues(http.StatusText(resp.StatusCode), req.Method, domain).Inc() + + return resp, nil +} + +// RoundTripper provides Prometheus instrumentation for an HTTP client, including domain labels. +func RoundTripper(baseTransport http.RoundTripper) http.RoundTripper { + if baseTransport == nil { + baseTransport = http.DefaultTransport + } + + return promhttp.InstrumentRoundTripperInFlight(clientInFlightGauge, + &tracingRoundTripper{base: baseTransport}, // Removed InstrumentRoundTripperCounter + ) +} From 68006d8cc72c43591d1a0497164c09c3e5a992ba Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 16:10:17 +0000 Subject: [PATCH 2/9] Fix up client/client.go and go.mod --- go.mod | 21 ++++++++++++++++++--- go.sum | 41 +++++++++++++++++++++++++++++++++++++---- pkg/client/client.go | 3 ++- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 748f1cbd..168d2697 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ 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.25.0 k8s.io/api v0.32.2 k8s.io/apimachinery v0.32.2 k8s.io/cli-runtime v0.32.2 @@ -31,17 +31,26 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.29.6 github.com/aws/aws-sdk-go-v2/credentials v1.17.59 github.com/aws/aws-sdk-go-v2/service/ecr v1.41.0 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/gofri/go-github-ratelimit v1.1.0 - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.3 + github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20250225234217-098045d5e61f github.com/google/go-github/v62 v62.0.0 + github.com/google/go-github/v70 v70.0.0 github.com/jarcoal/httpmock v1.3.1 + github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/stretchr/testify v1.10.0 ) require ( + cloud.google.com/go/compute/metadata v0.6.0 // indirect + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect 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/azure/auth v0.5.13 // 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 @@ -49,17 +58,20 @@ require ( github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.29.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 // indirect github.com/aws/smithy-go v1.22.2 // indirect + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20250115170608-608f37feb051 // 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/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // 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/dimchansky/utfbom v1.1.1 // indirect github.com/docker/cli v27.5.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect @@ -75,6 +87,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20241111191718-6bce25ecf029 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect @@ -88,6 +101,7 @@ require ( 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 + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -102,6 +116,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.61.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/vbatts/tar-split v0.11.6 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect diff --git a/go.sum b/go.sum index b92527db..52719662 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,22 @@ +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/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.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= +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= @@ -32,6 +42,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/service/ecr v1.41.0 h1:PNluoO7Sh1myhX+6MiAUpFk46fG6827K4U+KrtUT3s8= github.com/aws/aws-sdk-go-v2/service/ecr v1.41.0/go.mod h1:dtD3a4sjUjVL86e0NUvaqdGvds5ED6itUiZPDaT+Gh8= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.29.2 h1:h4q24ImESGfeamE0I0KJvsblO+03tn8J3+upacKf0vw= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.29.2/go.mod h1:3jWiVYuMsv18/qYLY6xVNe84CG/wKaa7vnLaH2/XtxI= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 h1:SYVGSFQHlchIcy6e7x12bsrxClCXSP5et8cqVhL8cuw= @@ -44,12 +56,16 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 h1:TzeR06UCMUq+KA3bDkujxK1GVGy github.com/aws/aws-sdk-go-v2/service/sts v1.33.14/go.mod h1:dspXf/oYWGWo6DEvj98wpaTeqt5+DMidZD0A9BYTizc= 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/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20250115170608-608f37feb051 h1:brogdiBXQBvbc+5SQoHOdfxbi77GyaUx6CpuepoEoC4= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20250115170608-608f37feb051/go.mod h1:B0Hkcs9+qs/7jvQ+YIIIJ2XKeSbJlkLMEKrz0+Ssgl0= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -59,6 +75,8 @@ 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/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.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= @@ -88,6 +106,7 @@ 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/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -101,12 +120,18 @@ github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63Kqpo 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.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-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= +github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20250225234217-098045d5e61f h1:LA+8uYrQl2biusGs1VEnIUQHLu8RjaCUNqHsieRkaTI= +github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20250225234217-098045d5e61f/go.mod h1:8mk2eu7HGqCp+JSWQVFCnKQwk/K6cIY6ID9aX72iTRo= +github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20241111191718-6bce25ecf029 h1:tmtax9EjrCFrrw72NeGso7qZUnJXTIP368kcjE4lZwE= +github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20241111191718-6bce25ecf029/go.mod h1:zD6WJVa49IK5fhrZOUaq7UcSgxZFlnS80EJBrcVFkFI= 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-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o= +github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY= 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= @@ -134,6 +159,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 h1:M8exrBzuhWcU6aoHJlHWPe4qFjVKzkMGRal78f5jRRU= +github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23/go.mod h1:kBSna6b0/RzsOcOZf515vAXwSsXYusl2U7SA0XP09yI= 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.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= @@ -152,6 +179,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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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= @@ -175,6 +204,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +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= @@ -190,6 +221,8 @@ github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFS github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= 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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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= @@ -315,8 +348,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.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= diff --git a/pkg/client/client.go b/pkg/client/client.go index b2f1ebae..f5da820c 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -44,6 +44,7 @@ type ImageClient interface { type Client struct { clients []ImageClient fallbackClient ImageClient + log *logrus.Entry } // Options used to configure client authentication. @@ -69,7 +70,7 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) return nil, fmt.Errorf("failed to create docker client: %s", err) } - var selfhostedClients []api.ImageClient + var selfhostedClients []ImageClient for _, sOpts := range opts.Selfhosted { sClient, err := selfhosted.New(ctx, log, sOpts) if err != nil { From 17187bafb56577265ce31f50abc46b6f973dfbd7 Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 16:17:27 +0000 Subject: [PATCH 3/9] Tidying things up a little --- go.mod | 11 ++++------- go.sum | 11 +++-------- pkg/client/client.go | 8 ++++---- pkg/client/selfhosted/selfhosted.go | 3 +++ 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 168d2697..9a81bc0c 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ 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 + golang.org/x/oauth2 v0.25.0 // indirect k8s.io/api v0.32.2 k8s.io/apimachinery v0.32.2 k8s.io/cli-runtime v0.32.2 @@ -31,15 +31,14 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.29.6 github.com/aws/aws-sdk-go-v2/credentials v1.17.59 github.com/aws/aws-sdk-go-v2/service/ecr v1.41.0 - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/go-chi/transport v0.5.0 github.com/gofri/go-github-ratelimit v1.1.0 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.3 github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20250225234217-098045d5e61f github.com/google/go-github/v62 v62.0.0 - 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/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/stretchr/testify v1.10.0 ) @@ -71,6 +70,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // 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/dimchansky/utfbom v1.1.1 // indirect github.com/docker/cli v27.5.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect @@ -93,7 +93,6 @@ 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 @@ -101,7 +100,6 @@ require ( 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 - github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -116,7 +114,6 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.61.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect github.com/vbatts/tar-split v0.11.6 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect diff --git a/go.sum b/go.sum index 52719662..283bc055 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,8 @@ 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= @@ -130,8 +132,6 @@ github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-2024111119171 github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20241111191718-6bce25ecf029/go.mod h1:zD6WJVa49IK5fhrZOUaq7UcSgxZFlnS80EJBrcVFkFI= 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-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o= -github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY= 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= @@ -159,8 +159,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 h1:M8exrBzuhWcU6aoHJlHWPe4qFjVKzkMGRal78f5jRRU= -github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23/go.mod h1:kBSna6b0/RzsOcOZf515vAXwSsXYusl2U7SA0XP09yI= 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.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= @@ -179,8 +177,6 @@ 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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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= @@ -221,8 +217,6 @@ github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFS github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= 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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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= @@ -289,6 +283,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ 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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/pkg/client/client.go b/pkg/client/client.go index f5da820c..d6bc6de8 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -63,18 +63,18 @@ type Options struct { 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 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) } @@ -83,7 +83,7 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) 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{ diff --git a/pkg/client/selfhosted/selfhosted.go b/pkg/client/selfhosted/selfhosted.go index 6488458f..c5cb4f23 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" From a16fa217e8932f1c6fa14623bc951c327ec35341 Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 16:20:53 +0000 Subject: [PATCH 4/9] Switch to cleanhttp as default Transport --- cmd/app/app.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/app/app.go b/cmd/app/app.go index d680960a..ffc7c5ba 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -7,6 +7,10 @@ import ( "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 @@ -57,7 +61,7 @@ func NewCommand(ctx context.Context) *cobra.Command { } opts.Client.Transport = transport.Chain( - http.DefaultTransport, + cleanhttp.DefaultTransport(), metrics.RoundTripper, ) From 790ba31d3d2a8f3eb930bd5e8f3fc8b274757c85 Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 16:36:06 +0000 Subject: [PATCH 5/9] Fix up for circle importing --- pkg/api/types.go | 20 ++++++++++++++++++++ pkg/client/client.go | 27 ++++----------------------- pkg/client/client_test.go | 3 ++- pkg/client/fallback/fallback.go | 9 +++++++-- pkg/controller/sync.go | 1 + 5 files changed, 34 insertions(+), 26 deletions(-) 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 d6bc6de8..28a037eb 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -20,30 +20,11 @@ import ( "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 } @@ -70,7 +51,7 @@ func New(ctx context.Context, log *logrus.Entry, opts Options) (*Client, error) 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 { @@ -119,7 +100,7 @@ func (c *Client) Tags(ctx context.Context, imageURL string) ([]api.ImageTag, err // 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/fallback/fallback.go b/pkg/client/fallback/fallback.go index a2326baf..502025bf 100644 --- a/pkg/client/fallback/fallback.go +++ b/pkg/client/fallback/fallback.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "time" "github.com/jetstack/version-checker/pkg/api" "github.com/jetstack/version-checker/pkg/client/oci" @@ -47,8 +48,12 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) (tags []api // 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 tags, err := client.Tags(ctx, host, repo, image); err == nil { - return tags, nil + 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) diff --git a/pkg/controller/sync.go b/pkg/controller/sync.go index 273b8674..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" From 879636104179e4a13dad82475931fad25bdfce85 Mon Sep 17 00:00:00 2001 From: David Collom Date: Mon, 24 Mar 2025 12:03:54 +0000 Subject: [PATCH 6/9] Resolve metrics re-registrying --- cmd/app/app.go | 9 +- cmd/app/helpers.go | 15 ++ pkg/controller/sync_test.go | 1 + pkg/metrics/metrics.go | 16 +- pkg/metrics/roundtripper.go | 143 ++++++++++-------- pkg/metrics/roundtripper_test.go | 241 +++++++++++++++++++++++++++++++ 6 files changed, 352 insertions(+), 73 deletions(-) create mode 100644 cmd/app/helpers.go create mode 100644 pkg/metrics/roundtripper_test.go diff --git a/cmd/app/app.go b/cmd/app/app.go index ffc7c5ba..beb7f8bc 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -3,7 +3,6 @@ package app import ( "context" "fmt" - "os" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -39,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 { @@ -62,7 +57,7 @@ func NewCommand(ctx context.Context) *cobra.Command { opts.Client.Transport = transport.Chain( cleanhttp.DefaultTransport(), - metrics.RoundTripper, + metricsServer.RoundTripper, ) client, err := client.New(ctx, log, opts.Client) 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/pkg/controller/sync_test.go b/pkg/controller/sync_test.go index 3ff59f03..a7fa876d 100644 --- a/pkg/controller/sync_test.go +++ b/pkg/controller/sync_test.go @@ -118,6 +118,7 @@ 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.NewServer(log) imageClient := &client.Client{} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index acdf45fc..413d8f60 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -20,10 +20,14 @@ import ( // metrics. type Metrics struct { *http.Server + log *logrus.Entry + registry *prometheus.Registry containerImageVersion *prometheus.GaugeVec containerImageDuration *prometheus.GaugeVec - log *logrus.Entry + + // Contains all metrics for the roundtripper + roundTripper *RoundTripper // container cache stores a cache of a container's current image, version, // and the latest @@ -38,7 +42,9 @@ type cacheItem struct { } func NewServer(log *logrus.Entry) *Metrics { - containerImageVersion := promauto.NewGaugeVec( + // Reset the prometheus registry + reg := prometheus.NewRegistry() + containerImageVersion := promauto.With(reg).NewGaugeVec( prometheus.GaugeOpts{ Namespace: "version_checker", Name: "is_latest_version", @@ -48,7 +54,7 @@ func NewServer(log *logrus.Entry) *Metrics { "namespace", "pod", "container", "container_type", "image", "current_version", "latest_version", }, ) - containerImageDuration := promauto.NewGaugeVec( + containerImageDuration := promauto.With(reg).NewGaugeVec( prometheus.GaugeOpts{ Namespace: "version_checker", Name: "image_lookup_duration", @@ -59,16 +65,18 @@ func NewServer(log *logrus.Entry) *Metrics { return &Metrics{ 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)) diff --git a/pkg/metrics/roundtripper.go b/pkg/metrics/roundtripper.go index de6642a3..152e4951 100644 --- a/pkg/metrics/roundtripper.go +++ b/pkg/metrics/roundtripper.go @@ -2,9 +2,11 @@ package metrics import ( "crypto/tls" + "net" "net/http" "net/http/httptrace" "net/url" + "strings" "time" "github.com/prometheus/client_golang/prometheus" @@ -12,49 +14,78 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) -var ( - clientInFlightGauge = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "client_in_flight_requests", - Help: "A gauge of in-flight requests for the wrapped client.", - Namespace: "http", - }) - - clientCounter = promauto.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 - ) +type RoundTripper struct { + base http.RoundTripper - histVec = promauto.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "client_request_duration_seconds", - Help: "A histogram of request durations.", - Namespace: "http", - }, - []string{"method", "domain"}, // Explicit labels - ) + clientInFlightGauge prometheus.Gauge + clientCounter *prometheus.CounterVec + histVec *prometheus.GaugeVec + tlsLatencyVec *prometheus.GaugeVec + dnsLatencyVec *prometheus.GaugeVec +} - tlsLatencyVec = promauto.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "tls_duration_seconds", - Help: "Trace TLS latency histogram.", - Namespace: "http", - }, - []string{"event", "domain"}, - ) +// 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 + } - dnsLatencyVec = promauto.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "dns_duration_seconds", - Help: "Trace DNS latency histogram.", - Namespace: "http", - }, - []string{"event", "domain"}, + 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 { @@ -65,15 +96,14 @@ func extractDomain(req *http.Request) string { if err != nil { return "unknown" } - return parsedURL.Hostname() -} - -// tracingRoundTripper wraps RoundTripper to track request metrics. -type tracingRoundTripper struct { - base http.RoundTripper + host := parsedURL.Hostname() + if strings.Contains(host, ":") { + host, _, _ = net.SplitHostPort(host) + } + return host } -func (t *tracingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { +func (t *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { domain := extractDomain(req) // Track request duration @@ -88,14 +118,14 @@ func (t *tracingRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro }, DNSDone: func(_ httptrace.DNSDoneInfo) { dnsEnd = time.Now() - dnsLatencyVec.WithLabelValues("dns_done", domain).Set(dnsEnd.Sub(dnsStart).Seconds()) + t.dnsLatencyVec.WithLabelValues("dns_done", domain).Set(dnsEnd.Sub(dnsStart).Seconds()) }, TLSHandshakeStart: func() { tlsStart = time.Now() }, TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { tlsEnd = time.Now() - tlsLatencyVec.WithLabelValues("tls_done", domain).Set(tlsEnd.Sub(tlsStart).Seconds()) + t.tlsLatencyVec.WithLabelValues("tls_done", domain).Set(tlsEnd.Sub(tlsStart).Seconds()) }, } @@ -105,27 +135,16 @@ func (t *tracingRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro resp, err := t.base.RoundTrip(req) // Manually record request duration - histVec.WithLabelValues(req.Method, domain).Set(time.Since(startTime).Seconds()) + t.histVec.WithLabelValues(req.Method, domain).Set(time.Since(startTime).Seconds()) if err != nil { // In case of failure, still increment counter - clientCounter.WithLabelValues("error", req.Method, domain).Inc() + t.clientCounter.WithLabelValues("error", req.Method, domain).Inc() return nil, err } // Increment counter with domain label - clientCounter.WithLabelValues(http.StatusText(resp.StatusCode), req.Method, domain).Inc() + t.clientCounter.WithLabelValues(http.StatusText(resp.StatusCode), req.Method, domain).Inc() return resp, nil } - -// RoundTripper provides Prometheus instrumentation for an HTTP client, including domain labels. -func RoundTripper(baseTransport http.RoundTripper) http.RoundTripper { - if baseTransport == nil { - baseTransport = http.DefaultTransport - } - - return promhttp.InstrumentRoundTripperInFlight(clientInFlightGauge, - &tracingRoundTripper{base: baseTransport}, // Removed InstrumentRoundTripperCounter - ) -} diff --git a/pkg/metrics/roundtripper_test.go b/pkg/metrics/roundtripper_test.go new file mode 100644 index 00000000..71ff74f2 --- /dev/null +++ b/pkg/metrics/roundtripper_test.go @@ -0,0 +1,241 @@ +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") + metricsServer := NewServer(log) + + 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: ``, + }, + { + name: "failed request", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + expectedStatus: http.StatusInternalServerError, + expectedError: false, + expectedMetricString: ` +http_client_in_flight_requests 0 +http_client_request_duration_seconds{domain="127.0.0.1",method="GET"} 0 +http_client_requests_total{code="Internal Server Error",domain="127.0.0.1",method="GET"} 0 +http_client_requests_total{code="OK",domain="127.0.0.1",method="GET"} 0`, + }, + { + name: "request with DNS and TLS latency", + handler: func(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) + w.WriteHeader(http.StatusOK) + }, + expectedStatus: http.StatusOK, + expectedError: false, + expectedMetricString: ``, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + 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", + )) + }) + } +} +func TestRoundTripperMetrics(t *testing.T) { + t.Skipf("Still need to fix these") + metricsServer := NewServer(log) + tests := []struct { + name string + handler http.HandlerFunc + expectedStatus int + expectedError bool + }{ + { + name: "successful request", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }, + expectedStatus: http.StatusOK, + expectedError: false, + }, + { + name: "failed request", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + expectedStatus: http.StatusInternalServerError, + expectedError: false, + }, + { + name: "request with DNS and TLS latency", + handler: func(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) + w.WriteHeader(http.StatusOK) + }, + expectedStatus: http.StatusOK, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + 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(""), + "http_client_in_flight_requests", + "http_client_requests_total", + "http_client_request_duration_seconds", + "http_tls_duration_seconds", + "http_dns_duration_seconds", + )) + }) + } +} From 61568f78f76477ebb46433153693e75227805654 Mon Sep 17 00:00:00 2001 From: David Collom Date: Mon, 24 Mar 2025 12:27:15 +0000 Subject: [PATCH 7/9] Fix up metrics tests for RoundTripper --- pkg/metrics/roundtripper_test.go | 124 ++++++++++--------------------- 1 file changed, 41 insertions(+), 83 deletions(-) diff --git a/pkg/metrics/roundtripper_test.go b/pkg/metrics/roundtripper_test.go index 71ff74f2..19c153ba 100644 --- a/pkg/metrics/roundtripper_test.go +++ b/pkg/metrics/roundtripper_test.go @@ -91,8 +91,8 @@ func TestExtractDomain(t *testing.T) { } func TestRoundTripper(t *testing.T) { - t.Skipf("Still need to fix these") - metricsServer := NewServer(log) + // t.Skipf("Still need to fix these") + t.Parallel() tests := []struct { name string @@ -106,9 +106,19 @@ func TestRoundTripper(t *testing.T) { handler: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }, - expectedStatus: http.StatusOK, - expectedError: false, - expectedMetricString: ``, + 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", @@ -118,25 +128,42 @@ func TestRoundTripper(t *testing.T) { expectedStatus: http.StatusInternalServerError, expectedError: false, expectedMetricString: ` -http_client_in_flight_requests 0 -http_client_request_duration_seconds{domain="127.0.0.1",method="GET"} 0 -http_client_requests_total{code="Internal Server Error",domain="127.0.0.1",method="GET"} 0 -http_client_requests_total{code="OK",domain="127.0.0.1",method="GET"} 0`, + # 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(100 * time.Millisecond) + time.Sleep(10 * time.Millisecond) w.WriteHeader(http.StatusOK) }, - expectedStatus: http.StatusOK, - expectedError: false, - expectedMetricString: ``, + 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() @@ -163,79 +190,10 @@ http_client_requests_total{code="OK",domain="127.0.0.1",method="GET"} 0`, metricsServer.registry, strings.NewReader(tt.expectedMetricString), "http_client_in_flight_requests", "http_client_requests_total", - "http_client_request_duration_seconds", + // "http_client_request_duration_seconds", "http_tls_duration_seconds", "http_dns_duration_seconds", )) }) } } -func TestRoundTripperMetrics(t *testing.T) { - t.Skipf("Still need to fix these") - metricsServer := NewServer(log) - tests := []struct { - name string - handler http.HandlerFunc - expectedStatus int - expectedError bool - }{ - { - name: "successful request", - handler: func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }, - expectedStatus: http.StatusOK, - expectedError: false, - }, - { - name: "failed request", - handler: func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - }, - expectedStatus: http.StatusInternalServerError, - expectedError: false, - }, - { - name: "request with DNS and TLS latency", - handler: func(w http.ResponseWriter, r *http.Request) { - time.Sleep(100 * time.Millisecond) - w.WriteHeader(http.StatusOK) - }, - expectedStatus: http.StatusOK, - expectedError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - 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(""), - "http_client_in_flight_requests", - "http_client_requests_total", - "http_client_request_duration_seconds", - "http_tls_duration_seconds", - "http_dns_duration_seconds", - )) - }) - } -} From 65c88a69714d7c817fc2d807d5b990ca08382442 Mon Sep 17 00:00:00 2001 From: David Collom Date: Mon, 24 Mar 2025 12:31:30 +0000 Subject: [PATCH 8/9] Fix up Metrics and allow for nil transporter on OCI Client --- pkg/client/oci/oci.go | 9 ++++++--- pkg/client/selfhosted/selfhosted_test.go | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/client/oci/oci.go b/pkg/client/oci/oci.go index 939bc8d4..f27fa1b9 100644 --- a/pkg/client/oci/oci.go +++ b/pkg/client/oci/oci.go @@ -35,9 +35,12 @@ type Client struct { // New returns a new client func New(opts *Options) (*Client, error) { - puller, err := remote.NewPuller( - remote.WithTransport(opts.Transporter), - ) + 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) } 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) { From 6866bf4bed16d098037e04dadaaa522a7efb7ebe Mon Sep 17 00:00:00 2001 From: David Collom Date: Mon, 24 Mar 2025 12:37:50 +0000 Subject: [PATCH 9/9] Upgrading jwt package due to CVE --- go.mod | 4 ++-- go.sum | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c0d84b2f..80b09dbd 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/Azure/go-autorest/autorest v0.11.30 github.com/Azure/go-autorest/autorest/adal v0.9.24 github.com/aws/aws-sdk-go-v2 v1.36.1 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/prometheus/client_golang v1.20.5 github.com/sirupsen/logrus v1.9.3 @@ -36,8 +36,8 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.3 github.com/google/go-containerregistry/pkg/authn/k8schain v0.0.0-20250225234217-098045d5e61f - github.com/hashicorp/go-cleanhttp v0.5.2 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 diff --git a/go.sum b/go.sum index 247dd76e..3129422e 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,8 @@ github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQg github.com/golang-jwt/jwt/v4 v4.5.1/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-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=