diff --git a/docs/operator-manual/metrics.md b/docs/operator-manual/metrics.md index 692d3ddab858b..3ac259390100f 100644 --- a/docs/operator-manual/metrics.md +++ b/docs/operator-manual/metrics.md @@ -255,14 +255,21 @@ Metrics about the Repo Server. The gRPC metrics are not exposed by default. Met Scraped at the `argocd-repo-server:8084/metrics` endpoint. -| Metric | Type | Description | -| --------------------------------------- | :-------: | ------------------------------------------------------------------------- | -| `argocd_git_request_duration_seconds` | histogram | Git requests duration seconds. | -| `argocd_git_request_total` | counter | Number of git requests performed by repo server | -| `argocd_git_fetch_fail_total` | counter | Number of git fetch requests failures by repo server | -| `argocd_redis_request_duration_seconds` | histogram | Redis requests duration seconds. | -| `argocd_redis_request_total` | counter | Number of Kubernetes requests executed during application reconciliation. | -| `argocd_repo_pending_request_total` | gauge | Number of pending requests requiring repository lock | +| Metric | Type | Description | +|------------------------------------------|:----------:|---------------------------------------------------------------------------| +| `argocd_git_request_duration_seconds` | histogram | Git requests duration seconds. | +| `argocd_git_request_total` | counter | Number of git requests performed by repo server | +| `argocd_git_fetch_fail_total` | counter | Number of git fetch requests failures by repo server | +| `argocd_redis_request_duration_seconds` | histogram | Redis requests duration seconds. | +| `argocd_redis_request_total` | counter | Number of Kubernetes requests executed during application reconciliation. | +| `argocd_repo_pending_request_total` | gauge | Number of pending requests requiring repository lock | +| `argocd_oci_request_total` | counter | Number of OCI requests performed by repo server | +| `argocd_oci_request_duration_seconds` | histogram | Number of OCI fetch requests failures by repo server | +| `argocd_oci_test_repo_fail_total` | counter | Number of OCI test repo requests failures by repo server | +| `argocd_oci_get_tags_fail_total` | counter | Number of OCI get tags requests failures by repo server | +| `argocd_oci_digest_metadata_fail_total` | counter | Number of OCI digest metadata failures by repo server | +| `argocd_oci_resolve_revision_fail_total` | counter | Number of OCI resolve revision failures by repo server | +| `argocd_oci_extract_fail_total` | counter | Number of OCI extract requests failures by repo server | ## Commit Server Metrics diff --git a/reposerver/metrics/metrics.go b/reposerver/metrics/metrics.go index 52c8ac592a7d1..88ea34d517279 100644 --- a/reposerver/metrics/metrics.go +++ b/reposerver/metrics/metrics.go @@ -11,15 +11,22 @@ import ( ) type MetricsServer struct { - handler http.Handler - gitFetchFailCounter *prometheus.CounterVec - gitLsRemoteFailCounter *prometheus.CounterVec - gitRequestCounter *prometheus.CounterVec - gitRequestHistogram *prometheus.HistogramVec - repoPendingRequestsGauge *prometheus.GaugeVec - redisRequestCounter *prometheus.CounterVec - redisRequestHistogram *prometheus.HistogramVec - PrometheusRegistry *prometheus.Registry + handler http.Handler + gitFetchFailCounter *prometheus.CounterVec + gitLsRemoteFailCounter *prometheus.CounterVec + gitRequestCounter *prometheus.CounterVec + gitRequestHistogram *prometheus.HistogramVec + repoPendingRequestsGauge *prometheus.GaugeVec + redisRequestCounter *prometheus.CounterVec + redisRequestHistogram *prometheus.HistogramVec + ociExtractFailCounter *prometheus.CounterVec + ociResolveRevisionFailCounter *prometheus.CounterVec + ociDigestMetadataCounter *prometheus.CounterVec + ociGetTagsFailCounter *prometheus.CounterVec + ociTestRepoFailCounter *prometheus.CounterVec + ociRequestCounter *prometheus.CounterVec + ociRequestHistogram *prometheus.HistogramVec + PrometheusRegistry *prometheus.Registry } type GitRequestType string @@ -100,16 +107,87 @@ func NewMetricsServer() *MetricsServer { ) registry.MustRegister(redisRequestHistogram) + ociExtractFailCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_oci_extract_fail_total", + Help: "Number of OCI extract requests failures by repo server", + }, + []string{"repo", "revision"}, + ) + registry.MustRegister(ociExtractFailCounter) + + ociResolveRevisionFailCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_oci_resolve_revision_fail_total", + Help: "Number of OCI resolve revision requests failures by repo server", + }, + []string{"repo", "revision"}, + ) + registry.MustRegister(ociResolveRevisionFailCounter) + + ociDigestMetadataCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_oci_digest_metadata_fail_total", + Help: "Number of OCI digest metadata requests failures by repo server", + }, + []string{"repo", "revision"}, + ) + registry.MustRegister(ociDigestMetadataCounter) + + ociGetTagsFailCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_oci_get_tags_fail_total", + Help: "Number of OCI get tags failures by repo server", + }, + []string{"repo"}, + ) + registry.MustRegister(ociGetTagsFailCounter) + + ociTestRepoFailCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_oci_test_repo_fail_total", + Help: "Number of OCI test repo requests failures by repo server", + }, + []string{"repo"}, + ) + registry.MustRegister(ociTestRepoFailCounter) + + ociRequestCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_oci_request_total", + Help: "Number of OCI requests performed by repo server", + }, + []string{"repo", "request_type"}, + ) + registry.MustRegister(ociRequestCounter) + + ociRequestHistogram := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "argocd_oci_request_duration_seconds", + Help: "OCI requests duration seconds.", + Buckets: []float64{0.1, 0.25, .5, 1, 2, 4, 10, 20}, + }, + []string{"repo", "request_type"}, + ) + registry.MustRegister(ociRequestHistogram) + return &MetricsServer{ - handler: promhttp.HandlerFor(registry, promhttp.HandlerOpts{}), - gitFetchFailCounter: gitFetchFailCounter, - gitLsRemoteFailCounter: gitLsRemoteFailCounter, - gitRequestCounter: gitRequestCounter, - gitRequestHistogram: gitRequestHistogram, - repoPendingRequestsGauge: repoPendingRequestsGauge, - redisRequestCounter: redisRequestCounter, - redisRequestHistogram: redisRequestHistogram, - PrometheusRegistry: registry, + handler: promhttp.HandlerFor(registry, promhttp.HandlerOpts{}), + gitFetchFailCounter: gitFetchFailCounter, + gitLsRemoteFailCounter: gitLsRemoteFailCounter, + gitRequestCounter: gitRequestCounter, + gitRequestHistogram: gitRequestHistogram, + repoPendingRequestsGauge: repoPendingRequestsGauge, + redisRequestCounter: redisRequestCounter, + redisRequestHistogram: redisRequestHistogram, + ociRequestCounter: ociRequestCounter, + ociRequestHistogram: ociRequestHistogram, + ociExtractFailCounter: ociExtractFailCounter, + ociResolveRevisionFailCounter: ociResolveRevisionFailCounter, + ociGetTagsFailCounter: ociGetTagsFailCounter, + ociDigestMetadataCounter: ociDigestMetadataCounter, + ociTestRepoFailCounter: ociTestRepoFailCounter, + PrometheusRegistry: registry, } } @@ -149,3 +227,37 @@ func (m *MetricsServer) IncRedisRequest(failed bool) { func (m *MetricsServer) ObserveRedisRequestDuration(duration time.Duration) { m.redisRequestHistogram.WithLabelValues("argocd-repo-server").Observe(duration.Seconds()) } + +// IncOCIRequest increments the OCI requests counter +func (m *MetricsServer) IncOCIRequest(repo string, requestType OCIRequestType) { + m.ociRequestCounter.WithLabelValues(repo, string(requestType)).Inc() +} + +func (m *MetricsServer) ObserveOCIRequestDuration(repo string, requestType OCIRequestType, duration time.Duration) { + m.ociRequestHistogram.WithLabelValues(repo, string(requestType)).Observe(duration.Seconds()) +} + +// IncOCIExtractFailCounter increments the OCI failed extract requests counter +func (m *MetricsServer) IncOCIExtractFailCounter(repo string, revision string) { + m.ociExtractFailCounter.WithLabelValues(repo, revision).Inc() +} + +// IncOCIResolveRevisionFailCounter increments the OCI failed resolve revision requests counter +func (m *MetricsServer) IncOCIResolveRevisionFailCounter(repo string, revision string) { + m.ociResolveRevisionFailCounter.WithLabelValues(repo, revision).Inc() +} + +// IncOCIDigestMetadataCounter increments the OCI failed digest metadata requests counter +func (m *MetricsServer) IncOCIDigestMetadataCounter(repo string, revision string) { + m.ociDigestMetadataCounter.WithLabelValues(repo, revision).Inc() +} + +// IncOCIGetTagsFailCounter increments the OCI failed get tags requests counter +func (m *MetricsServer) IncOCIGetTagsFailCounter(repo string) { + m.ociGetTagsFailCounter.WithLabelValues(repo).Inc() +} + +// IncOCITestRepoFailCounter increments the OCI failed test repo requests counter +func (m *MetricsServer) IncOCITestRepoFailCounter(repo string) { + m.ociTestRepoFailCounter.WithLabelValues(repo).Inc() +} diff --git a/reposerver/metrics/ocihandlers.go b/reposerver/metrics/ocihandlers.go new file mode 100644 index 0000000000000..80cc891c4798d --- /dev/null +++ b/reposerver/metrics/ocihandlers.go @@ -0,0 +1,61 @@ +package metrics + +import ( + "time" + + "github.com/argoproj/argo-cd/v3/util/oci" +) + +type OCIRequestType string + +const ( + OCIRequestTypeExtract = "extract" + OCIRequestTypeResolveRevision = "resolve-revision" + OCIRequestTypeDigestMetadata = "digest-metadata" + OCIRequestTypeGetTags = "get-tags" + OCIRequestTypeTestRepo = "test-repo" +) + +// NewOCIClientEventHandlers creates event handlers to update OCI repo, related metrics +func NewOCIClientEventHandlers(metricsServer *MetricsServer) oci.EventHandlers { + return oci.EventHandlers{ + OnExtract: func(repo string) func() { + return processMetricFunc(metricsServer, repo, OCIRequestTypeExtract) + }, + OnResolveRevision: func(repo string) func() { + return processMetricFunc(metricsServer, repo, OCIRequestTypeResolveRevision) + }, + OnDigestMetadata: func(repo string) func() { + return processMetricFunc(metricsServer, repo, OCIRequestTypeDigestMetadata) + }, + OnGetTags: func(repo string) func() { + return processMetricFunc(metricsServer, repo, OCIRequestTypeGetTags) + }, + OnTestRepo: func(repo string) func() { + return processMetricFunc(metricsServer, repo, OCIRequestTypeTestRepo) + }, + OnExtractFail: func(repo string) func(revision string) { + return func(revision string) { metricsServer.IncOCIExtractFailCounter(repo, revision) } + }, + OnResolveRevisionFail: func(repo string) func(revision string) { + return func(revision string) { metricsServer.IncOCIResolveRevisionFailCounter(repo, revision) } + }, + OnDigestMetadataFail: func(repo string) func(revision string) { + return func(revision string) { metricsServer.IncOCIDigestMetadataCounter(repo, revision) } + }, + OnGetTagsFail: func(repo string) func() { + return func() { metricsServer.IncOCIGetTagsFailCounter(repo) } + }, + OnTestRepoFail: func(repo string) func() { + return func() { metricsServer.IncOCITestRepoFailCounter(repo) } + }, + } +} + +func processMetricFunc(metricsServer *MetricsServer, repo string, requestType OCIRequestType) func() { + startTime := time.Now() + metricsServer.IncOCIRequest(repo, requestType) + return func() { + metricsServer.ObserveOCIRequestDuration(repo, requestType, time.Since(startTime)) + } +} diff --git a/reposerver/metrics/ocihandlers_test.go b/reposerver/metrics/ocihandlers_test.go new file mode 100644 index 0000000000000..ec080f8428ab1 --- /dev/null +++ b/reposerver/metrics/ocihandlers_test.go @@ -0,0 +1,77 @@ +package metrics + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" +) + +func TestOCIClientEventHandlers(t *testing.T) { + tests := []struct { + name string + setup func() + teardown func() + testFunc func(t *testing.T) + }{ + { + name: "test event handlers", + testFunc: func(t *testing.T) { + t.Helper() + revision := "1.2.3" + assert.NotPanics(t, func() { + metricsServer := NewMetricsServer() + eventHandlers := NewOCIClientEventHandlers(metricsServer) + eventHandlers.OnExtract("test")() + eventHandlers.OnTestRepo("test")() + eventHandlers.OnGetTags("test")() + eventHandlers.OnResolveRevision("test")() + eventHandlers.OnDigestMetadata("test")() + eventHandlers.OnExtractFail("test")(revision) + eventHandlers.OnTestRepoFail("test")() + eventHandlers.OnGetTagsFail("test")() + eventHandlers.OnResolveRevisionFail("test")(revision) + eventHandlers.OnDigestMetadataFail("test")(revision) + c := metricsServer.ociRequestCounter + assert.Equal(t, 5, testutil.CollectAndCount(c)) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test", OCIRequestTypeExtract)), 0.01) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test", OCIRequestTypeResolveRevision)), 0.01) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test", OCIRequestTypeDigestMetadata)), 0.01) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test", OCIRequestTypeTestRepo)), 0.01) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test", OCIRequestTypeTestRepo)), 0.01) + + c = metricsServer.ociDigestMetadataCounter + assert.Equal(t, 1, testutil.CollectAndCount(c)) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test", revision)), 0.01) + + c = metricsServer.ociTestRepoFailCounter + assert.Equal(t, 1, testutil.CollectAndCount(c)) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test")), 0.01) + + c = metricsServer.ociExtractFailCounter + assert.Equal(t, 1, testutil.CollectAndCount(c)) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test", revision)), 0.01) + + c = metricsServer.ociGetTagsFailCounter + assert.Equal(t, 1, testutil.CollectAndCount(c)) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test")), 0.01) + + c = metricsServer.ociResolveRevisionFailCounter + assert.Equal(t, 1, testutil.CollectAndCount(c)) + assert.InDelta(t, float64(1), testutil.ToFloat64(c.WithLabelValues("test", revision)), 0.01) + }) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + if tt.teardown != nil { + defer tt.teardown() + } + tt.testFunc(t) + }) + } +} diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go index 659a2cec33803..b4cd9187c7036 100644 --- a/reposerver/repository/repository.go +++ b/reposerver/repository/repository.go @@ -190,7 +190,7 @@ func (s *Service) Init() error { // ListOCITags List a subset of the refs (currently, branches and tags) of a git repo func (s *Service) ListOCITags(ctx context.Context, q *apiclient.ListRefsRequest) (*apiclient.Refs, error) { - ociClient, err := s.newOCIClient(q.Repo.Repo, q.Repo.GetOCICreds(), q.Repo.Proxy, q.Repo.NoProxy, s.initConstants.OCIMediaTypes, oci.WithIndexCache(s.cache), oci.WithImagePaths(s.ociPaths), oci.WithManifestMaxExtractedSize(s.initConstants.OCIManifestMaxExtractedSize), oci.WithDisableManifestMaxExtractedSize(s.initConstants.DisableOCIManifestMaxExtractedSize)) + ociClient, err := s.newOCIClient(q.Repo.Repo, q.Repo.GetOCICreds(), q.Repo.Proxy, q.Repo.NoProxy, s.initConstants.OCIMediaTypes, s.ociClientStandardOpts()...) if err != nil { return nil, fmt.Errorf("error creating oci client: %w", err) } @@ -2493,13 +2493,14 @@ func (s *Service) GetRevisionMetadata(_ context.Context, q *apiclient.RepoServer } func (s *Service) GetOCIMetadata(ctx context.Context, q *apiclient.RepoServerRevisionChartDetailsRequest) (*v1alpha1.OCIMetadata, error) { - client, err := s.newOCIClient(q.Repo.Repo, q.Repo.GetOCICreds(), q.Repo.Proxy, q.Repo.NoProxy, s.initConstants.OCIMediaTypes, oci.WithIndexCache(s.cache), oci.WithImagePaths(s.ociPaths), oci.WithManifestMaxExtractedSize(s.initConstants.OCIManifestMaxExtractedSize), oci.WithDisableManifestMaxExtractedSize(s.initConstants.DisableOCIManifestMaxExtractedSize)) + client, err := s.newOCIClient(q.Repo.Repo, q.Repo.GetOCICreds(), q.Repo.Proxy, q.Repo.NoProxy, s.initConstants.OCIMediaTypes, s.ociClientStandardOpts()...) if err != nil { return nil, fmt.Errorf("failed to initialize oci client: %w", err) } metadata, err := client.DigestMetadata(ctx, q.Revision) if err != nil { + s.metricsServer.IncOCIDigestMetadataCounter(q.Repo.Repo, q.Revision) return nil, fmt.Errorf("failed to extract digest metadata for revision %q: %w", q.Revision, err) } @@ -2589,7 +2590,7 @@ func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision s } func (s *Service) newOCIClientResolveRevision(ctx context.Context, repo *v1alpha1.Repository, revision string, noRevisionCache bool) (oci.Client, string, error) { - ociClient, err := s.newOCIClient(repo.Repo, repo.GetOCICreds(), repo.Proxy, repo.NoProxy, s.initConstants.OCIMediaTypes, oci.WithIndexCache(s.cache), oci.WithImagePaths(s.ociPaths), oci.WithManifestMaxExtractedSize(s.initConstants.OCIManifestMaxExtractedSize), oci.WithDisableManifestMaxExtractedSize(s.initConstants.DisableOCIManifestMaxExtractedSize)) + ociClient, err := s.newOCIClient(repo.Repo, repo.GetOCICreds(), repo.Proxy, repo.NoProxy, s.initConstants.OCIMediaTypes, s.ociClientStandardOpts()...) if err != nil { return nil, "", fmt.Errorf("failed to initialize oci client: %w", err) } @@ -2786,7 +2787,8 @@ func (s *Service) TestRepository(ctx context.Context, q *apiclient.TestRepositor return git.TestRepo(repo.Repo, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy, repo.NoProxy) }, "oci": func() error { - client, err := oci.NewClient(repo.Repo, repo.GetOCICreds(), repo.Proxy, repo.NoProxy, s.initConstants.OCIMediaTypes) + client, err := oci.NewClient(repo.Repo, repo.GetOCICreds(), repo.Proxy, repo.NoProxy, + s.initConstants.OCIMediaTypes, oci.WithEventHandlers(metrics.NewOCIClientEventHandlers(s.metricsServer))) if err != nil { return err } @@ -3139,3 +3141,13 @@ func (s *Service) updateCachedRevision(logCtx *log.Entry, oldRev string, newRev logCtx.Debugf("manifest cache updated for application %s in repo %s from revision %s to revision %s", request.AppName, request.GetRepo().Repo, oldRev, newRev) return nil } + +func (s *Service) ociClientStandardOpts() []oci.ClientOpts { + return []oci.ClientOpts{ + oci.WithIndexCache(s.cache), + oci.WithImagePaths(s.ociPaths), + oci.WithManifestMaxExtractedSize(s.initConstants.OCIManifestMaxExtractedSize), + oci.WithDisableManifestMaxExtractedSize(s.initConstants.DisableOCIManifestMaxExtractedSize), + oci.WithEventHandlers(metrics.NewOCIClientEventHandlers(s.metricsServer)), + } +} diff --git a/util/oci/client.go b/util/oci/client.go index e4db909c1f5a8..1a18a60e64ed7 100644 --- a/util/oci/client.go +++ b/util/oci/client.go @@ -192,8 +192,23 @@ func newClientWithLock(repoURL string, repoLock sync.KeyLock, repo oras.ReadOnly return c } +type EventHandlers struct { + OnExtract func(repo string) func() + OnResolveRevision func(repo string) func() + OnDigestMetadata func(repo string) func() + OnTestRepo func(repo string) func() + OnGetTags func(repo string) func() + OnExtractFail func(repo string) func(revision string) + OnResolveRevisionFail func(repo string) func(revision string) + OnDigestMetadataFail func(repo string) func(revision string) + OnTestRepoFail func(repo string) func() + OnGetTagsFail func(repo string) func() +} + // nativeOCIClient implements Client interface using oras-go type nativeOCIClient struct { + EventHandlers + repoURL string repo oras.ReadOnlyTarget tagsFunc func(context.Context, string) ([]string, error) @@ -208,11 +223,18 @@ type nativeOCIClient struct { // TestRepo verifies that the remote OCI repo can be connected to. func (c *nativeOCIClient) TestRepo(ctx context.Context) (bool, error) { + defer c.OnTestRepo(c.repoURL)() + err := c.pingFunc(ctx) + if err != nil { + defer c.OnTestRepoFail(c.repoURL)() + } return err == nil, err } func (c *nativeOCIClient) Extract(ctx context.Context, digest string) (string, utilio.Closer, error) { + defer c.OnExtract(c.repoURL)() + cachedPath, err := c.getCachedPath(digest) if err != nil { return "", nil, fmt.Errorf("error getting oci path for digest %s: %w", digest, err) @@ -229,6 +251,7 @@ func (c *nativeOCIClient) Extract(ctx context.Context, digest string) (string, u if !exists { ociManifest, err := getOCIManifest(ctx, digest, c.repo) if err != nil { + defer c.OnExtractFail(c.repoURL)(digest) return "", nil, err } @@ -295,6 +318,8 @@ func (c *nativeOCIClient) CleanCache(revision string) error { // DigestMetadata extracts the OCI manifest for a given revision and returns it to the caller. func (c *nativeOCIClient) DigestMetadata(ctx context.Context, digest string) (*imagev1.Manifest, error) { + defer c.OnDigestMetadata(c.repoURL)() + path, err := c.getCachedPath(digest) if err != nil { return nil, fmt.Errorf("error fetching oci metadata path for digest %s: %w", digest, err) @@ -309,6 +334,8 @@ func (c *nativeOCIClient) DigestMetadata(ctx context.Context, digest string) (*i } func (c *nativeOCIClient) ResolveRevision(ctx context.Context, revision string, noCache bool) (string, error) { + defer c.OnResolveRevision(c.repoURL)() + digest, err := c.resolveDigest(ctx, revision) // Lookup explicit revision if err != nil { // If the revision is not a semver constraint, just return the error @@ -334,6 +361,7 @@ func (c *nativeOCIClient) ResolveRevision(ctx context.Context, revision string, } func (c *nativeOCIClient) GetTags(ctx context.Context, noCache bool) ([]string, error) { + defer c.OnGetTags(c.repoURL)() indexLock.Lock(c.repoURL) defer indexLock.Unlock(c.repoURL) @@ -349,6 +377,7 @@ func (c *nativeOCIClient) GetTags(ctx context.Context, noCache bool) ([]string, start := time.Now() result, err := c.tagsFunc(ctx, "") if err != nil { + defer c.OnDigestMetadataFail(c.repoURL) return nil, fmt.Errorf("failed to get tags: %w", err) } @@ -378,6 +407,7 @@ func (c *nativeOCIClient) GetTags(ctx context.Context, noCache bool) ([]string, func (c *nativeOCIClient) resolveDigest(ctx context.Context, revision string) (string, error) { descriptor, err := c.repo.Resolve(ctx, revision) if err != nil { + defer c.OnResolveRevisionFail(c.repoURL)(revision) return "", fmt.Errorf("cannot get digest for revision %s: %w", revision, err) } @@ -611,3 +641,10 @@ func getOCIManifest(ctx context.Context, digest string, repo oras.ReadOnlyTarget return &manifest, nil } + +// WithEventHandlers sets the git client event handlers +func WithEventHandlers(handlers EventHandlers) ClientOpts { + return func(c *nativeOCIClient) { + c.EventHandlers = handlers + } +} diff --git a/util/oci/client_test.go b/util/oci/client_test.go index 1372c128b827a..2b964c28c8bc9 100644 --- a/util/oci/client_test.go +++ b/util/oci/client_test.go @@ -270,7 +270,11 @@ func Test_nativeOCIClient_Extract(t *testing.T) { store := memory.New() c := newClientWithLock(fields.repoURL, globalLock, store, fields.tagsFunc, func(_ context.Context) error { return nil - }, fields.allowedMediaTypes, WithImagePaths(cacheDir), WithManifestMaxExtractedSize(args.manifestMaxExtractedSize), WithDisableManifestMaxExtractedSize(args.disableManifestMaxExtractedSize)) + }, fields.allowedMediaTypes, + WithImagePaths(cacheDir), + WithManifestMaxExtractedSize(args.manifestMaxExtractedSize), + WithDisableManifestMaxExtractedSize(args.disableManifestMaxExtractedSize), + WithEventHandlers(fakeEventHandlers(t, fields.repoURL))) _, gotCloser, err := c.Extract(t.Context(), sha) require.NoError(t, err) require.NoError(t, gotCloser.Close()) @@ -286,7 +290,11 @@ func Test_nativeOCIClient_Extract(t *testing.T) { c := newClientWithLock(tt.fields.repoURL, globalLock, store, tt.fields.tagsFunc, func(_ context.Context) error { return nil - }, tt.fields.allowedMediaTypes, WithImagePaths(cacheDir), WithManifestMaxExtractedSize(tt.args.manifestMaxExtractedSize), WithDisableManifestMaxExtractedSize(tt.args.disableManifestMaxExtractedSize)) + }, tt.fields.allowedMediaTypes, + WithImagePaths(cacheDir), + WithManifestMaxExtractedSize(tt.args.manifestMaxExtractedSize), + WithDisableManifestMaxExtractedSize(tt.args.disableManifestMaxExtractedSize), + WithEventHandlers(fakeEventHandlers(t, tt.fields.repoURL))) path, gotCloser, err := c.Extract(t.Context(), sha) if tt.expectedError != nil { @@ -436,7 +444,7 @@ func Test_nativeOCIClient_ResolveRevision(t *testing.T) { t.Run(tt.name, func(t *testing.T) { c := newClientWithLock(tt.fields.repoURL, globalLock, tt.fields.repo, tt.fields.tagsFunc, func(_ context.Context) error { return nil - }, tt.fields.allowedMediaTypes) + }, tt.fields.allowedMediaTypes, WithEventHandlers(fakeEventHandlers(t, tt.fields.repoURL))) got, err := c.ResolveRevision(t.Context(), tt.revision, tt.noCache) if tt.expectedError != nil { require.EqualError(t, err, tt.expectedError.Error()) @@ -450,3 +458,23 @@ func Test_nativeOCIClient_ResolveRevision(t *testing.T) { }) } } + +func fakeEventHandlers(t *testing.T, repoURL string) EventHandlers { + t.Helper() + return EventHandlers{ + OnExtract: func(repo string) func() { return func() { require.Equal(t, repoURL, repo) } }, + OnResolveRevision: func(repo string) func() { return func() { require.Equal(t, repoURL, repo) } }, + OnDigestMetadata: func(repo string) func() { return func() { require.Equal(t, repoURL, repo) } }, + OnTestRepo: func(repo string) func() { return func() { require.Equal(t, repoURL, repo) } }, + OnGetTags: func(repo string) func() { return func() { require.Equal(t, repoURL, repo) } }, + OnExtractFail: func(repo string) func(revision string) { + return func(_ string) { require.Equal(t, repoURL, repo) } + }, + OnResolveRevisionFail: func(repo string) func(revision string) { + return func(_ string) { require.Equal(t, repoURL, repo) } + }, + OnDigestMetadataFail: func(repo string) func(revision string) { + return func(_ string) { require.Equal(t, repoURL, repo) } + }, + } +}