Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ require (
github.com/MakeNowJust/heredoc v1.0.0
github.com/Maldris/go-billy-afero v0.0.0-20200815120323-e9d3de59c99a
github.com/conforma/crds/api v0.1.7
github.com/conforma/go-gather v1.1.0
github.com/conforma/go-gather v1.2.0
github.com/docker/docker v28.5.2+incompatible
github.com/evanphx/json-patch v5.9.11+incompatible
github.com/gkampitakis/go-snaps v0.5.19
github.com/go-git/go-git/v5 v5.17.1
github.com/go-git/go-git/v5 v5.18.0
github.com/go-logr/logr v1.4.3
github.com/go-openapi/strfmt v0.26.1
github.com/google/go-cmp v0.7.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,8 @@ github.com/conforma/crds/api v0.1.7 h1:IBsIw35A5QkP5i1Se6/BASaFvTnW+xTlWEqJMdX7E
github.com/conforma/crds/api v0.1.7/go.mod h1:vaUCN54r99HME4555KRKOUPGJxBNAlX1agZ4jlGI2YA=
github.com/conforma/go-containerregistry v0.20.7-0.20251103083939-3459088e4bae h1:KoJk+DwW+UReJ47IFxJvEqlc3jOTzsXum5mQ85MEogg=
github.com/conforma/go-containerregistry v0.20.7-0.20251103083939-3459088e4bae/go.mod h1:sdc2GWaYnuP7aY7cx4nTEW16xVJsVuqjZ+zyx5S0XeI=
github.com/conforma/go-gather v1.1.0 h1:j7n9J66gW9d4Imlvx1sBJUC2fmquf/u9jxAJjIu14EY=
github.com/conforma/go-gather v1.1.0/go.mod h1:75GiwN11ojMb6i00hTAMaFhTkaiDf34a93BdWhnC4NM=
github.com/conforma/go-gather v1.2.0 h1:HYmUtkGPQRUOb2mEBQ51FTSO0SpZ//12mNp2AXcVxBs=
github.com/conforma/go-gather v1.2.0/go.mod h1:7LbJh5AAljGbOoUWQy+wBKu7MKecyG45bLbbe7Bo0/A=
github.com/containerd/containerd/v2 v2.2.2 h1:mjVQdtfryzT7lOqs5EYUFZm8ioPVjOpkSoG1GJPxEMY=
github.com/containerd/containerd/v2 v2.2.2/go.mod h1:5Jhevmv6/2J+Iu/A2xXAdUIdI5Ah/hfyO7okJ4AFIdY=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
Expand Down Expand Up @@ -480,8 +480,8 @@ github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDz
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.17.1 h1:WnljyxIzSj9BRRUlnmAU35ohDsjRK0EKmL0evDqi5Jk=
github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand Down
44 changes: 38 additions & 6 deletions internal/downloader/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package downloader
import (
"context"
"fmt"
net_http "net/http"
"regexp"
"strings"
"sync"
Expand All @@ -37,25 +38,53 @@ type key int

const downloadImplKey key = 0

// downloadImpl defines the interface for downloading files.
type downloadImpl interface {
Download(context.Context, string, []string) error
}

var log = logrus.StandardLogger()

// ociGatherer and httpGatherer hold gatherer instances configured with
// tracing and retry transports via WithTransport. Initialized once by
// _initialize via sync.OnceFunc.
var (
ociGatherer *goci.OCIGatherer
httpGatherer *ghttp.HTTPGatherer
)

// gatherFunc dispatches sources to the appropriate gatherer. OCI and HTTP
// scheme prefixes route to custom gatherers; all other sources fall through
// to the go-gather registry.
var gatherFunc = func(ctx context.Context, source, destination string) (metadata.Metadata, error) {
initialize()

// Dispatch to custom gatherers only for unambiguous scheme prefixes.
// Bare hostnames (e.g. quay.io/..., 127.0.0.1/...) fall through to
// the registry, which checks git matchers before OCI matchers.
switch {
case strings.HasPrefix(source, "oci://") || strings.HasPrefix(source, "oci::"):
return ociGatherer.Gather(ctx, source, destination)
case strings.HasPrefix(source, "https://") || strings.HasPrefix(source, "http://"):
if httpGatherer.Matcher(source) {
return httpGatherer.Gather(ctx, source, destination)
}
}

g, err := registry.GetGatherer(source)
if err != nil {
return nil, err
}
return g.Gather(ctx, source, destination)
}

// _initialize builds the transport stack (optional tracing + retry) and
// constructs the OCI and HTTP gatherer instances.
var _initialize = func() {
var base net_http.RoundTripper = net_http.DefaultTransport

if log.IsLevelEnabled(logrus.TraceLevel) {
goci.Transport = http.NewTracingRoundTripperWithLogger(goci.Transport)
ghttp.Transport = http.NewTracingRoundTripperWithLogger(ghttp.Transport)
base = http.NewTracingRoundTripperWithLogger(base)
}

backoff := retry.ExponentialBackoff(http.DefaultBackoff.Duration, http.DefaultBackoff.Factor, http.DefaultBackoff.Jitter)
Expand All @@ -69,13 +98,16 @@ var _initialize = func() {
return policy
}

ociTransport := retry.NewTransport(goci.Transport)
ociTransport := retry.NewTransport(base)
ociTransport.Policy = policyfn
goci.Transport = ociTransport
oci := goci.NewOCIGatherer(goci.WithTransport(ociTransport))

httpTransport := retry.NewTransport(ghttp.Transport)
httpTransport := retry.NewTransport(base)
httpTransport.Policy = policyfn
ghttp.Transport = httpTransport
h := ghttp.NewHTTPGatherer(ghttp.WithTransport(httpTransport))

ociGatherer = oci
httpGatherer = h
}

var initialize = sync.OnceFunc(_initialize)
Expand Down
39 changes: 23 additions & 16 deletions internal/downloader/downloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,11 @@ func TestOCITracing(t *testing.T) {
t.Cleanup(trace.Stop)

log.Level = logrus.TraceLevel
initialize = _initialize // we want it to re-execute for the test
t.Cleanup(func() {
log.Level = logrus.InfoLevel
initialize = sync.OnceFunc(_initialize)
goci.Transport = http.DefaultTransport
ghttp.Transport = http.DefaultTransport
ociGatherer = nil
httpGatherer = nil
})
registry := httptest.NewServer(registry.New())
t.Cleanup(registry.Close)
Expand All @@ -179,7 +178,12 @@ func TestOCITracing(t *testing.T) {
require.NoError(t, err)
require.NoError(t, remote.Push(ref, img))

_, err = gatherFunc(context.Background(), ref.String(), t.TempDir())
// Initialize directly then call ociGatherer.Gather to test the OCI
// tracing path. Bare localhost refs fall through to the registry's
// default gatherer (no tracing), so we invoke the gatherer directly.
_initialize()

_, err = ociGatherer.Gather(context.Background(), ref.String(), t.TempDir())
require.NoError(t, err)

trace.Stop()
Expand Down Expand Up @@ -215,12 +219,12 @@ func TestHTTPTracing(t *testing.T) {
t.Cleanup(trace.Stop)

log.Level = logrus.TraceLevel
initialize = _initialize // we want it to re-execute for the test
initialize = _initialize
t.Cleanup(func() {
log.Level = logrus.InfoLevel
initialize = sync.OnceFunc(_initialize)
goci.Transport = http.DefaultTransport
ghttp.Transport = http.DefaultTransport
ociGatherer = nil
httpGatherer = nil
})
var requests []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -261,32 +265,35 @@ func TestHTTPTracing(t *testing.T) {
assert.Equal(t, 3, found)
}

// TestOCIClientConfiguration verifies _initialize constructs an OCI gatherer.
// OCIGatherer.transport is unexported, so we cannot verify the retry transport
// directly (unlike TestHTTPClientConfiguration). TestOCITracing provides
// integration-level verification that the transport stack is wired correctly.
func TestOCIClientConfiguration(t *testing.T) {
defaultMaxRetry := echttp.DefaultRetry.MaxRetry
t.Cleanup(func() {
echttp.DefaultRetry.MaxRetry = defaultMaxRetry
ociGatherer = nil
httpGatherer = nil
})
echttp.DefaultRetry.MaxRetry = rand.Int() //nolint:gosec // G404 - no need for a secure random here

_initialize()

assert.IsType(t, &retry.Transport{}, goci.Transport)

transport := goci.Transport.(*retry.Transport)
assert.Equal(t, echttp.DefaultRetry.MaxRetry, transport.Policy().(*retry.GenericPolicy).MaxRetry)
assert.IsType(t, &goci.OCIGatherer{}, ociGatherer)
}

func TestHTTPClientConfiguration(t *testing.T) {
defaultMaxRetry := echttp.DefaultRetry.MaxRetry
t.Cleanup(func() {
echttp.DefaultRetry.MaxRetry = defaultMaxRetry
ociGatherer = nil
httpGatherer = nil
})
echttp.DefaultRetry.MaxRetry = rand.Int() //nolint:gosec // G404 - no need for a secure random here

_initialize()

assert.IsType(t, &retry.Transport{}, ghttp.Transport)
require.IsType(t, &ghttp.HTTPGatherer{}, httpGatherer)
assert.IsType(t, &retry.Transport{}, httpGatherer.Client.Transport)

transport := ghttp.Transport.(*retry.Transport)
transport := httpGatherer.Client.Transport.(*retry.Transport)
assert.Equal(t, echttp.DefaultRetry.MaxRetry, transport.Policy().(*retry.GenericPolicy).MaxRetry)
}
Loading