feat: scaffold native registry push package with go-containerregistry dep
- commit
- f8212fb2546d4df8582f639df02fdae084326d4b
- author
- brice · · 2026-03-14 13:53 UTC
- parent
- d1b6a91
- signature
- unsigned
4 files · +105 -13
M go.mod +3 -4
| @@ -10,6 +10,7 @@ require ( | |||
| 10 | 10 | github.com/docker/docker v28.5.2+incompatible | |
| 11 | 11 | github.com/docker/go-connections v0.5.0 | |
| 12 | 12 | github.com/golang-jwt/jwt/v5 v5.3.1 | |
| 13 | + | github.com/google/go-containerregistry v0.21.2 | |
| 13 | 14 | github.com/google/uuid v1.6.0 | |
| 14 | 15 | github.com/mattn/go-runewidth v0.0.21 | |
| 15 | 16 | github.com/rivo/uniseg v0.4.7 | |
| @@ -43,6 +44,7 @@ require ( | |||
| 43 | 44 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect | |
| 44 | 45 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect | |
| 45 | 46 | github.com/mattn/go-localereader v0.0.1 // indirect | |
| 47 | + | github.com/moby/sys/sequential v0.6.0 // indirect | |
| 46 | 48 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | |
| 47 | 49 | github.com/muesli/cancelreader v0.2.2 // indirect | |
| 48 | 50 | github.com/muesli/termenv v0.16.0 // indirect | |
| @@ -68,10 +70,9 @@ require ( | |||
| 68 | 70 | ) | |
| 69 | 71 | ||
| 70 | 72 | require ( | |
| 71 | - | github.com/Microsoft/go-winio v0.4.14 // indirect | |
| 73 | + | github.com/Microsoft/go-winio v0.6.2 // indirect | |
| 72 | 74 | github.com/containerd/errdefs v1.0.0 | |
| 73 | 75 | github.com/containerd/errdefs/pkg v0.3.0 // indirect | |
| 74 | - | github.com/containerd/log v0.1.0 // indirect | |
| 75 | 76 | github.com/distribution/reference v0.6.0 // indirect | |
| 76 | 77 | github.com/docker/go-units v0.5.0 // indirect | |
| 77 | 78 | github.com/felixge/httpsnoop v1.0.4 // indirect | |
| @@ -83,9 +84,7 @@ require ( | |||
| 83 | 84 | github.com/mattn/go-colorable v0.1.14 // indirect | |
| 84 | 85 | github.com/mattn/go-isatty v0.0.20 // indirect | |
| 85 | 86 | github.com/moby/docker-image-spec v1.3.1 // indirect | |
| 86 | - | github.com/moby/sys/atomicwriter v0.1.0 // indirect | |
| 87 | 87 | github.com/moby/term v0.5.2 // indirect | |
| 88 | - | github.com/morikuni/aec v1.0.0 // indirect | |
| 89 | 88 | github.com/opencontainers/go-digest v1.0.0 // indirect | |
| 90 | 89 | github.com/opencontainers/image-spec v1.1.1 // indirect | |
| 91 | 90 | github.com/pelletier/go-toml/v2 v2.2.4 | |
| @@ -10,6 +10,7 @@ require ( | |||||
| 10 | github.com/docker/docker v28.5.2+incompatible | 10 | github.com/docker/docker v28.5.2+incompatible | ||
| 11 | github.com/docker/go-connections v0.5.0 | 11 | github.com/docker/go-connections v0.5.0 | ||
| 12 | github.com/golang-jwt/jwt/v5 v5.3.1 | 12 | github.com/golang-jwt/jwt/v5 v5.3.1 | ||
| 13 | + | github.com/google/go-containerregistry v0.21.2 | |||
| 13 | github.com/google/uuid v1.6.0 | 14 | github.com/google/uuid v1.6.0 | ||
| 14 | github.com/mattn/go-runewidth v0.0.21 | 15 | github.com/mattn/go-runewidth v0.0.21 | ||
| 15 | github.com/rivo/uniseg v0.4.7 | 16 | github.com/rivo/uniseg v0.4.7 | ||
| @@ -43,6 +44,7 @@ require ( | |||||
| 43 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect | 44 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect | ||
| 44 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect | 45 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect | ||
| 45 | github.com/mattn/go-localereader v0.0.1 // indirect | 46 | github.com/mattn/go-localereader v0.0.1 // indirect | ||
| 47 | + | github.com/moby/sys/sequential v0.6.0 // indirect | |||
| 46 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | 48 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | ||
| 47 | github.com/muesli/cancelreader v0.2.2 // indirect | 49 | github.com/muesli/cancelreader v0.2.2 // indirect | ||
| 48 | github.com/muesli/termenv v0.16.0 // indirect | 50 | github.com/muesli/termenv v0.16.0 // indirect | ||
| @@ -68,10 +70,9 @@ require ( | |||||
| 68 | ) | 70 | ) | ||
| 69 | 71 | ||||
| 70 | require ( | 72 | require ( | ||
| 71 | - | github.com/Microsoft/go-winio v0.4.14 // indirect | 73 | + | github.com/Microsoft/go-winio v0.6.2 // indirect |
| 72 | github.com/containerd/errdefs v1.0.0 | 74 | github.com/containerd/errdefs v1.0.0 | ||
| 73 | github.com/containerd/errdefs/pkg v0.3.0 // indirect | 75 | github.com/containerd/errdefs/pkg v0.3.0 // indirect | ||
| 74 | - | github.com/containerd/log v0.1.0 // indirect | |||
| 75 | github.com/distribution/reference v0.6.0 // indirect | 76 | github.com/distribution/reference v0.6.0 // indirect | ||
| 76 | github.com/docker/go-units v0.5.0 // indirect | 77 | github.com/docker/go-units v0.5.0 // indirect | ||
| 77 | github.com/felixge/httpsnoop v1.0.4 // indirect | 78 | github.com/felixge/httpsnoop v1.0.4 // indirect | ||
| @@ -83,9 +84,7 @@ require ( | |||||
| 83 | github.com/mattn/go-colorable v0.1.14 // indirect | 84 | github.com/mattn/go-colorable v0.1.14 // indirect | ||
| 84 | github.com/mattn/go-isatty v0.0.20 // indirect | 85 | github.com/mattn/go-isatty v0.0.20 // indirect | ||
| 85 | github.com/moby/docker-image-spec v1.3.1 // indirect | 86 | github.com/moby/docker-image-spec v1.3.1 // indirect | ||
| 86 | - | github.com/moby/sys/atomicwriter v0.1.0 // indirect | |||
| 87 | github.com/moby/term v0.5.2 // indirect | 87 | github.com/moby/term v0.5.2 // indirect | ||
| 88 | - | github.com/morikuni/aec v1.0.0 // indirect | |||
| 89 | github.com/opencontainers/go-digest v1.0.0 // indirect | 88 | github.com/opencontainers/go-digest v1.0.0 // indirect | ||
| 90 | github.com/opencontainers/image-spec v1.1.1 // indirect | 89 | github.com/opencontainers/image-spec v1.1.1 // indirect | ||
| 91 | github.com/pelletier/go-toml/v2 v2.2.4 | 90 | github.com/pelletier/go-toml/v2 v2.2.4 | ||
M go.sum +4 -9 generated file: collapsed
A pkg/registrypush/push.go +79 -0
| @@ -0,0 +1,79 @@ | |||
| 1 | + | package registrypush | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "io" | |
| 6 | + | "net/http" | |
| 7 | + | ||
| 8 | + | v1 "github.com/google/go-containerregistry/pkg/v1" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // DefaultChunkSize is 50MB - safely under Cloudflare's 100MB per-request limit | |
| 12 | + | // and Gordon's default max_blob_chunk_size of 95MB. | |
| 13 | + | const DefaultChunkSize = 50 * 1024 * 1024 | |
| 14 | + | ||
| 15 | + | // ImageSource provides a v1.Image for a given ref. | |
| 16 | + | // The default reads from the local Docker/Podman daemon. | |
| 17 | + | // Override with WithImageSource for testing. | |
| 18 | + | type ImageSource func(ctx context.Context, ref string) (v1.Image, error) | |
| 19 | + | ||
| 20 | + | // Pusher uploads container images to a registry using chunked blob uploads. | |
| 21 | + | type Pusher struct { | |
| 22 | + | chunkSize int64 | |
| 23 | + | transport http.RoundTripper | |
| 24 | + | imageSource ImageSource | |
| 25 | + | progress io.Writer | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Option configures a Pusher. | |
| 29 | + | type Option func(*Pusher) | |
| 30 | + | ||
| 31 | + | // WithChunkSize sets the maximum size of each blob upload chunk. | |
| 32 | + | func WithChunkSize(size int64) Option { | |
| 33 | + | return func(p *Pusher) { p.chunkSize = size } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // WithTransport sets a custom HTTP transport (for testing). | |
| 37 | + | func WithTransport(t http.RoundTripper) Option { | |
| 38 | + | return func(p *Pusher) { p.transport = t } | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // WithImageSource overrides the default daemon-based image reader. | |
| 42 | + | func WithImageSource(src ImageSource) Option { | |
| 43 | + | return func(p *Pusher) { p.imageSource = src } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // WithProgress sets a writer for progress messages. | |
| 47 | + | func WithProgress(w io.Writer) Option { | |
| 48 | + | return func(p *Pusher) { p.progress = w } | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // New creates a Pusher with the given options. | |
| 52 | + | func New(opts ...Option) *Pusher { | |
| 53 | + | p := &Pusher{chunkSize: DefaultChunkSize} | |
| 54 | + | for _, o := range opts { | |
| 55 | + | o(p) | |
| 56 | + | } | |
| 57 | + | if p.transport == nil { | |
| 58 | + | p.transport = http.DefaultTransport | |
| 59 | + | } | |
| 60 | + | if p.imageSource == nil { | |
| 61 | + | p.imageSource = defaultImageSource | |
| 62 | + | } | |
| 63 | + | return p | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Push reads a local image (by ref) from the Docker/Podman daemon and uploads | |
| 67 | + | // it to the registry encoded in the ref, using chunked blob uploads. | |
| 68 | + | // | |
| 69 | + | // The ref must be fully qualified: registry.example.com/name:tag | |
| 70 | + | func (p *Pusher) Push(ctx context.Context, ref string) error { | |
| 71 | + | // Implementation in subsequent tasks | |
| 72 | + | return nil | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // defaultImageSource reads an image from the local Docker/Podman daemon. | |
| 76 | + | func defaultImageSource(ctx context.Context, ref string) (v1.Image, error) { | |
| 77 | + | // Implementation in Task 3 | |
| 78 | + | return nil, nil | |
| 79 | + | } | |
| @@ -0,0 +1,79 @@ | |||||
| 1 | + | package registrypush | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "io" | |||
| 6 | + | "net/http" | |||
| 7 | + | ||||
| 8 | + | v1 "github.com/google/go-containerregistry/pkg/v1" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // DefaultChunkSize is 50MB - safely under Cloudflare's 100MB per-request limit | |||
| 12 | + | // and Gordon's default max_blob_chunk_size of 95MB. | |||
| 13 | + | const DefaultChunkSize = 50 * 1024 * 1024 | |||
| 14 | + | ||||
| 15 | + | // ImageSource provides a v1.Image for a given ref. | |||
| 16 | + | // The default reads from the local Docker/Podman daemon. | |||
| 17 | + | // Override with WithImageSource for testing. | |||
| 18 | + | type ImageSource func(ctx context.Context, ref string) (v1.Image, error) | |||
| 19 | + | ||||
| 20 | + | // Pusher uploads container images to a registry using chunked blob uploads. | |||
| 21 | + | type Pusher struct { | |||
| 22 | + | chunkSize int64 | |||
| 23 | + | transport http.RoundTripper | |||
| 24 | + | imageSource ImageSource | |||
| 25 | + | progress io.Writer | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Option configures a Pusher. | |||
| 29 | + | type Option func(*Pusher) | |||
| 30 | + | ||||
| 31 | + | // WithChunkSize sets the maximum size of each blob upload chunk. | |||
| 32 | + | func WithChunkSize(size int64) Option { | |||
| 33 | + | return func(p *Pusher) { p.chunkSize = size } | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // WithTransport sets a custom HTTP transport (for testing). | |||
| 37 | + | func WithTransport(t http.RoundTripper) Option { | |||
| 38 | + | return func(p *Pusher) { p.transport = t } | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // WithImageSource overrides the default daemon-based image reader. | |||
| 42 | + | func WithImageSource(src ImageSource) Option { | |||
| 43 | + | return func(p *Pusher) { p.imageSource = src } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // WithProgress sets a writer for progress messages. | |||
| 47 | + | func WithProgress(w io.Writer) Option { | |||
| 48 | + | return func(p *Pusher) { p.progress = w } | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // New creates a Pusher with the given options. | |||
| 52 | + | func New(opts ...Option) *Pusher { | |||
| 53 | + | p := &Pusher{chunkSize: DefaultChunkSize} | |||
| 54 | + | for _, o := range opts { | |||
| 55 | + | o(p) | |||
| 56 | + | } | |||
| 57 | + | if p.transport == nil { | |||
| 58 | + | p.transport = http.DefaultTransport | |||
| 59 | + | } | |||
| 60 | + | if p.imageSource == nil { | |||
| 61 | + | p.imageSource = defaultImageSource | |||
| 62 | + | } | |||
| 63 | + | return p | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // Push reads a local image (by ref) from the Docker/Podman daemon and uploads | |||
| 67 | + | // it to the registry encoded in the ref, using chunked blob uploads. | |||
| 68 | + | // | |||
| 69 | + | // The ref must be fully qualified: registry.example.com/name:tag | |||
| 70 | + | func (p *Pusher) Push(ctx context.Context, ref string) error { | |||
| 71 | + | // Implementation in subsequent tasks | |||
| 72 | + | return nil | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // defaultImageSource reads an image from the local Docker/Podman daemon. | |||
| 76 | + | func defaultImageSource(ctx context.Context, ref string) (v1.Image, error) { | |||
| 77 | + | // Implementation in Task 3 | |||
| 78 | + | return nil, nil | |||
| 79 | + | } | |||
A pkg/registrypush/push_test.go +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | package registrypush_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/assert" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/gordon/pkg/registrypush" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestNew_defaults(t *testing.T) { | |
| 12 | + | p := registrypush.New() | |
| 13 | + | assert.NotNil(t, p) | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | func TestNew_withChunkSize(t *testing.T) { | |
| 17 | + | p := registrypush.New(registrypush.WithChunkSize(10 * 1024 * 1024)) | |
| 18 | + | assert.NotNil(t, p) | |
| 19 | + | } | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | package registrypush_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/assert" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/gordon/pkg/registrypush" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestNew_defaults(t *testing.T) { | |||
| 12 | + | p := registrypush.New() | |||
| 13 | + | assert.NotNil(t, p) | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | func TestNew_withChunkSize(t *testing.T) { | |||
| 17 | + | p := registrypush.New(registrypush.WithChunkSize(10 * 1024 * 1024)) | |||
| 18 | + | assert.NotNil(t, p) | |||
| 19 | + | } | |||