detached 8fce1c3

fix: honor TLS insecure settings in native registry push and refactor docker shell-outs

Replace docker CLI shell-outs (tag, inspect) with Docker SDK calls and introduce
pushImageOps interface for testability. The native registry push now resolves
insecure TLS from --insecure flag, env, and client config — matching the remote
client behavior.

- Add WithInsecureTLS option to registrypush.Pusher with safe transport fallback
- Add pushImageOps interface + dockerImageOps implementation (Tag, Push, Exists, Build)
- Replace dockerTagFn/dockerPushFn/dockerImageExistsFn function variables
- Build method returns actionable error for non-Docker runtimes (Podman)
- Generate mockery mock for pushImageOps, use in all tests

Closes #107
commit
8fce1c332815171e1487c0acaee89f9f5aa24304
author
brice · · 2026-03-14 17:14 UTC
parent
d176775
signature
unsigned
10 files · +551 -112
M .mockery.yaml +3 -0
@@ -35,3 +35,6 @@ packages:
3535 HTTPProber:
3636 SecretService:
3737 LogService:
38+ github.com/bnema/gordon/internal/adapters/in/cli:
39+ interfaces:
40+ pushImageOps:
@@ -35,3 +35,6 @@ packages:
35 HTTPProber:35 HTTPProber:
36 SecretService:36 SecretService:
37 LogService:37 LogService:
38+ github.com/bnema/gordon/internal/adapters/in/cli:
39+ interfaces:
40+ pushImageOps:
M internal/adapters/in/cli/attachments_push.go +9 -6
@@ -15,8 +15,6 @@ import (
1515
1616 var (
1717 resolveControlPlaneFn = resolveControlPlane
18- tagAndPushFn = tagAndPush
19- buildAndPushFn = buildAndPush
2018 determineVersionFn = determineVersion
2119 )
2220
@@ -77,11 +75,16 @@ func runAttachmentsPush(ctx context.Context, imageArg, tag string, build bool, p
7775
7876 versionRef, latestRef := resolveImageRefs(registry, imageName, version)
7977
78+ imageOps, err := newImageOpsFn()
79+ if err != nil {
80+ return err
81+ }
82+
8083 if err := printAttachmentPushInfo(out, imageArg, targets, versionRef, latestRef, version); err != nil {
8184 return err
8285 }
8386
84- if err := performAttachmentPush(ctx, build, version, platform, dockerfile, buildArgs, registry, imageName, versionRef, latestRef); err != nil {
87+ if err := performAttachmentPush(ctx, imageOps, build, version, platform, dockerfile, buildArgs, registry, imageName, versionRef, latestRef); err != nil {
8588 return err
8689 }
8790
@@ -106,11 +109,11 @@ func printAttachmentPushInfo(out io.Writer, imageArg string, targets []string, v
106109 return nil
107110 }
108111
109-func performAttachmentPush(ctx context.Context, build bool, version, platform, dockerfile string, buildArgs []string, registry, imageName, versionRef, latestRef string) error {
112+func performAttachmentPush(ctx context.Context, ops pushImageOps, build bool, version, platform, dockerfile string, buildArgs []string, registry, imageName, versionRef, latestRef string) error {
110113 if build {
111- return buildAndPushFn(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)
114+ return buildAndPush(ctx, ops, version, platform, dockerfile, buildArgs, versionRef, latestRef)
112115 }
113- return tagAndPushFn(ctx, registry, imageName, version, versionRef, latestRef)
116+ return tagAndPush(ctx, ops, registry, imageName, version, versionRef, latestRef)
114117 }
115118
116119 func resolveAttachmentImage(ctx context.Context, cp ControlPlane, imageArg string) (registry, imageName string, targets []string, err error) {
@@ -15,8 +15,6 @@ import (
15 15
16 var (16 var (
17 resolveControlPlaneFn = resolveControlPlane17 resolveControlPlaneFn = resolveControlPlane
18- tagAndPushFn = tagAndPush
19- buildAndPushFn = buildAndPush
20 determineVersionFn = determineVersion18 determineVersionFn = determineVersion
21 )19 )
22 20
@@ -77,11 +75,16 @@ func runAttachmentsPush(ctx context.Context, imageArg, tag string, build bool, p
77 75
78 versionRef, latestRef := resolveImageRefs(registry, imageName, version)76 versionRef, latestRef := resolveImageRefs(registry, imageName, version)
79 77
78+ imageOps, err := newImageOpsFn()
79+ if err != nil {
80+ return err
81+ }
82+
80 if err := printAttachmentPushInfo(out, imageArg, targets, versionRef, latestRef, version); err != nil {83 if err := printAttachmentPushInfo(out, imageArg, targets, versionRef, latestRef, version); err != nil {
81 return err84 return err
82 }85 }
83 86
84- if err := performAttachmentPush(ctx, build, version, platform, dockerfile, buildArgs, registry, imageName, versionRef, latestRef); err != nil {87+ if err := performAttachmentPush(ctx, imageOps, build, version, platform, dockerfile, buildArgs, registry, imageName, versionRef, latestRef); err != nil {
85 return err88 return err
86 }89 }
87 90
@@ -106,11 +109,11 @@ func printAttachmentPushInfo(out io.Writer, imageArg string, targets []string, v
106 return nil109 return nil
107 }110 }
108 111
109-func performAttachmentPush(ctx context.Context, build bool, version, platform, dockerfile string, buildArgs []string, registry, imageName, versionRef, latestRef string) error {112+func performAttachmentPush(ctx context.Context, ops pushImageOps, build bool, version, platform, dockerfile string, buildArgs []string, registry, imageName, versionRef, latestRef string) error {
110 if build {113 if build {
111- return buildAndPushFn(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)114+ return buildAndPush(ctx, ops, version, platform, dockerfile, buildArgs, versionRef, latestRef)
112 }115 }
113- return tagAndPushFn(ctx, registry, imageName, version, versionRef, latestRef)116+ return tagAndPush(ctx, ops, registry, imageName, version, versionRef, latestRef)
114 }117 }
115 118
116 func resolveAttachmentImage(ctx context.Context, cp ControlPlane, imageArg string) (registry, imageName string, targets []string, err error) {119 func resolveAttachmentImage(ctx context.Context, cp ControlPlane, imageArg string) (registry, imageName string, targets []string, err error) {
M internal/adapters/in/cli/attachments_push_test.go +29 -13
@@ -6,9 +6,11 @@ import (
66 "testing"
77
88 "github.com/bnema/gordon/internal/adapters/dto"
9+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
910 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
1011 "github.com/bnema/gordon/internal/domain"
1112 "github.com/stretchr/testify/assert"
13+ "github.com/stretchr/testify/mock"
1214 "github.com/stretchr/testify/require"
1315 )
1416
@@ -212,12 +214,12 @@ func TestResolveAttachmentImage_NotConfigured(t *testing.T) {
212214
213215 func TestAttachmentPushCmd_NoDeploy(t *testing.T) {
214216 origResolveControlPlane := resolveControlPlaneFn
215- origTagAndPush := tagAndPushFn
216217 origDetermineVersion := determineVersionFn
218+ origNewImageOps := newImageOpsFn
217219 t.Cleanup(func() {
218220 resolveControlPlaneFn = origResolveControlPlane
219- tagAndPushFn = origTagAndPush
220221 determineVersionFn = origDetermineVersion
222+ newImageOpsFn = origNewImageOps
221223 })
222224
223225 cp := &attachmentPushTestControlPlane{
@@ -233,10 +235,15 @@ func TestAttachmentPushCmd_NoDeploy(t *testing.T) {
233235 return &controlPlaneHandle{plane: cp}, nil
234236 }
235237 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }
236- called := false
237- tagAndPushFn = func(context.Context, string, string, string, string, string) error {
238- called = true
239- return nil
238+ pushCalled := false
239+ newImageOpsFn = func() (pushImageOps, error) {
240+ m := climocks.NewMockpushImageOps(t)
241+ m.On("Tag", mock.Anything, mock.Anything, mock.Anything).Return(nil)
242+ m.On("Exists", mock.Anything, mock.Anything).Return(true, nil)
243+ m.On("Push", mock.Anything, mock.Anything).Run(func(mock.Arguments) {
244+ pushCalled = true
245+ }).Return(nil)
246+ return m, nil
240247 }
241248
242249 cmd := newAttachmentsPushCmd()
@@ -248,18 +255,18 @@ func TestAttachmentPushCmd_NoDeploy(t *testing.T) {
248255 err := cmd.Execute()
249256
250257 require.NoError(t, err)
251- assert.True(t, called)
258+ assert.True(t, pushCalled)
252259 assert.Contains(t, out.String(), "Push complete")
253260 }
254261
255262 func TestAttachmentPushCmd_TaggedImageInputBuildsValidRefs(t *testing.T) {
256263 origResolveControlPlane := resolveControlPlaneFn
257- origTagAndPush := tagAndPushFn
258264 origDetermineVersion := determineVersionFn
265+ origNewImageOps := newImageOpsFn
259266 t.Cleanup(func() {
260267 resolveControlPlaneFn = origResolveControlPlane
261- tagAndPushFn = origTagAndPush
262268 determineVersionFn = origDetermineVersion
269+ newImageOpsFn = origNewImageOps
263270 })
264271
265272 cp := &attachmentPushTestControlPlane{
@@ -278,10 +285,19 @@ func TestAttachmentPushCmd_TaggedImageInputBuildsValidRefs(t *testing.T) {
278285
279286 var gotVersionRef string
280287 var gotLatestRef string
281- tagAndPushFn = func(_ context.Context, registry, imageName, version, versionRef, latestRef string) error {
282- gotVersionRef = versionRef
283- gotLatestRef = latestRef
284- return nil
288+ newImageOpsFn = func() (pushImageOps, error) {
289+ m := climocks.NewMockpushImageOps(t)
290+ m.On("Tag", mock.Anything, mock.Anything, mock.Anything).Return(nil)
291+ m.On("Exists", mock.Anything, mock.Anything).Return(true, nil)
292+ m.On("Push", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
293+ ref := args.Get(1).(string)
294+ if gotVersionRef == "" {
295+ gotVersionRef = ref
296+ } else if gotLatestRef == "" {
297+ gotLatestRef = ref
298+ }
299+ }).Return(nil)
300+ return m, nil
285301 }
286302
287303 cmd := newAttachmentsPushCmd()
@@ -6,9 +6,11 @@ import (
6 "testing"6 "testing"
7 7
8 "github.com/bnema/gordon/internal/adapters/dto"8 "github.com/bnema/gordon/internal/adapters/dto"
9+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
9 "github.com/bnema/gordon/internal/adapters/in/cli/remote"10 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
10 "github.com/bnema/gordon/internal/domain"11 "github.com/bnema/gordon/internal/domain"
11 "github.com/stretchr/testify/assert"12 "github.com/stretchr/testify/assert"
13+ "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"14 "github.com/stretchr/testify/require"
13 )15 )
14 16
@@ -212,12 +214,12 @@ func TestResolveAttachmentImage_NotConfigured(t *testing.T) {
212 214
213 func TestAttachmentPushCmd_NoDeploy(t *testing.T) {215 func TestAttachmentPushCmd_NoDeploy(t *testing.T) {
214 origResolveControlPlane := resolveControlPlaneFn216 origResolveControlPlane := resolveControlPlaneFn
215- origTagAndPush := tagAndPushFn
216 origDetermineVersion := determineVersionFn217 origDetermineVersion := determineVersionFn
218+ origNewImageOps := newImageOpsFn
217 t.Cleanup(func() {219 t.Cleanup(func() {
218 resolveControlPlaneFn = origResolveControlPlane220 resolveControlPlaneFn = origResolveControlPlane
219- tagAndPushFn = origTagAndPush
220 determineVersionFn = origDetermineVersion221 determineVersionFn = origDetermineVersion
222+ newImageOpsFn = origNewImageOps
221 })223 })
222 224
223 cp := &attachmentPushTestControlPlane{225 cp := &attachmentPushTestControlPlane{
@@ -233,10 +235,15 @@ func TestAttachmentPushCmd_NoDeploy(t *testing.T) {
233 return &controlPlaneHandle{plane: cp}, nil235 return &controlPlaneHandle{plane: cp}, nil
234 }236 }
235 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }237 determineVersionFn = func(context.Context, string) string { return "v1.2.3" }
236- called := false238+ pushCalled := false
237- tagAndPushFn = func(context.Context, string, string, string, string, string) error {239+ newImageOpsFn = func() (pushImageOps, error) {
238- called = true240+ m := climocks.NewMockpushImageOps(t)
239- return nil241+ m.On("Tag", mock.Anything, mock.Anything, mock.Anything).Return(nil)
242+ m.On("Exists", mock.Anything, mock.Anything).Return(true, nil)
243+ m.On("Push", mock.Anything, mock.Anything).Run(func(mock.Arguments) {
244+ pushCalled = true
245+ }).Return(nil)
246+ return m, nil
240 }247 }
241 248
242 cmd := newAttachmentsPushCmd()249 cmd := newAttachmentsPushCmd()
@@ -248,18 +255,18 @@ func TestAttachmentPushCmd_NoDeploy(t *testing.T) {
248 err := cmd.Execute()255 err := cmd.Execute()
249 256
250 require.NoError(t, err)257 require.NoError(t, err)
251- assert.True(t, called)258+ assert.True(t, pushCalled)
252 assert.Contains(t, out.String(), "Push complete")259 assert.Contains(t, out.String(), "Push complete")
253 }260 }
254 261
255 func TestAttachmentPushCmd_TaggedImageInputBuildsValidRefs(t *testing.T) {262 func TestAttachmentPushCmd_TaggedImageInputBuildsValidRefs(t *testing.T) {
256 origResolveControlPlane := resolveControlPlaneFn263 origResolveControlPlane := resolveControlPlaneFn
257- origTagAndPush := tagAndPushFn
258 origDetermineVersion := determineVersionFn264 origDetermineVersion := determineVersionFn
265+ origNewImageOps := newImageOpsFn
259 t.Cleanup(func() {266 t.Cleanup(func() {
260 resolveControlPlaneFn = origResolveControlPlane267 resolveControlPlaneFn = origResolveControlPlane
261- tagAndPushFn = origTagAndPush
262 determineVersionFn = origDetermineVersion268 determineVersionFn = origDetermineVersion
269+ newImageOpsFn = origNewImageOps
263 })270 })
264 271
265 cp := &attachmentPushTestControlPlane{272 cp := &attachmentPushTestControlPlane{
@@ -278,10 +285,19 @@ func TestAttachmentPushCmd_TaggedImageInputBuildsValidRefs(t *testing.T) {
278 285
279 var gotVersionRef string286 var gotVersionRef string
280 var gotLatestRef string287 var gotLatestRef string
281- tagAndPushFn = func(_ context.Context, registry, imageName, version, versionRef, latestRef string) error {288+ newImageOpsFn = func() (pushImageOps, error) {
282- gotVersionRef = versionRef289+ m := climocks.NewMockpushImageOps(t)
283- gotLatestRef = latestRef290+ m.On("Tag", mock.Anything, mock.Anything, mock.Anything).Return(nil)
284- return nil291+ m.On("Exists", mock.Anything, mock.Anything).Return(true, nil)
292+ m.On("Push", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
293+ ref := args.Get(1).(string)
294+ if gotVersionRef == "" {
295+ gotVersionRef = ref
296+ } else if gotLatestRef == "" {
297+ gotLatestRef = ref
298+ }
299+ }).Return(nil)
300+ return m, nil
285 }301 }
286 302
287 cmd := newAttachmentsPushCmd()303 cmd := newAttachmentsPushCmd()
A internal/adapters/in/cli/mocks/mock_push_image_ops.go +281 -0
@@ -0,0 +1,281 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewMockpushImageOps creates a new instance of MockpushImageOps. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewMockpushImageOps(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *MockpushImageOps {
19+ mock := &MockpushImageOps{}
20+ mock.Mock.Test(t)
21+
22+ t.Cleanup(func() { mock.AssertExpectations(t) })
23+
24+ return mock
25+}
26+
27+// MockpushImageOps is an autogenerated mock type for the pushImageOps type
28+type MockpushImageOps struct {
29+ mock.Mock
30+}
31+
32+type MockpushImageOps_Expecter struct {
33+ mock *mock.Mock
34+}
35+
36+func (_m *MockpushImageOps) EXPECT() *MockpushImageOps_Expecter {
37+ return &MockpushImageOps_Expecter{mock: &_m.Mock}
38+}
39+
40+// Build provides a mock function for the type MockpushImageOps
41+func (_mock *MockpushImageOps) Build(ctx context.Context, args []string) error {
42+ ret := _mock.Called(ctx, args)
43+
44+ if len(ret) == 0 {
45+ panic("no return value specified for Build")
46+ }
47+
48+ var r0 error
49+ if returnFunc, ok := ret.Get(0).(func(context.Context, []string) error); ok {
50+ r0 = returnFunc(ctx, args)
51+ } else {
52+ r0 = ret.Error(0)
53+ }
54+ return r0
55+}
56+
57+// MockpushImageOps_Build_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Build'
58+type MockpushImageOps_Build_Call struct {
59+ *mock.Call
60+}
61+
62+// Build is a helper method to define mock.On call
63+// - ctx context.Context
64+// - args []string
65+func (_e *MockpushImageOps_Expecter) Build(ctx interface{}, args interface{}) *MockpushImageOps_Build_Call {
66+ return &MockpushImageOps_Build_Call{Call: _e.mock.On("Build", ctx, args)}
67+}
68+
69+func (_c *MockpushImageOps_Build_Call) Run(run func(ctx context.Context, args []string)) *MockpushImageOps_Build_Call {
70+ _c.Call.Run(func(args mock.Arguments) {
71+ var arg0 context.Context
72+ if args[0] != nil {
73+ arg0 = args[0].(context.Context)
74+ }
75+ var arg1 []string
76+ if args[1] != nil {
77+ arg1 = args[1].([]string)
78+ }
79+ run(
80+ arg0,
81+ arg1,
82+ )
83+ })
84+ return _c
85+}
86+
87+func (_c *MockpushImageOps_Build_Call) Return(err error) *MockpushImageOps_Build_Call {
88+ _c.Call.Return(err)
89+ return _c
90+}
91+
92+func (_c *MockpushImageOps_Build_Call) RunAndReturn(run func(ctx context.Context, args []string) error) *MockpushImageOps_Build_Call {
93+ _c.Call.Return(run)
94+ return _c
95+}
96+
97+// Exists provides a mock function for the type MockpushImageOps
98+func (_mock *MockpushImageOps) Exists(ctx context.Context, imageRef string) (bool, error) {
99+ ret := _mock.Called(ctx, imageRef)
100+
101+ if len(ret) == 0 {
102+ panic("no return value specified for Exists")
103+ }
104+
105+ var r0 bool
106+ var r1 error
107+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
108+ return returnFunc(ctx, imageRef)
109+ }
110+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok {
111+ r0 = returnFunc(ctx, imageRef)
112+ } else {
113+ r0 = ret.Get(0).(bool)
114+ }
115+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
116+ r1 = returnFunc(ctx, imageRef)
117+ } else {
118+ r1 = ret.Error(1)
119+ }
120+ return r0, r1
121+}
122+
123+// MockpushImageOps_Exists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exists'
124+type MockpushImageOps_Exists_Call struct {
125+ *mock.Call
126+}
127+
128+// Exists is a helper method to define mock.On call
129+// - ctx context.Context
130+// - imageRef string
131+func (_e *MockpushImageOps_Expecter) Exists(ctx interface{}, imageRef interface{}) *MockpushImageOps_Exists_Call {
132+ return &MockpushImageOps_Exists_Call{Call: _e.mock.On("Exists", ctx, imageRef)}
133+}
134+
135+func (_c *MockpushImageOps_Exists_Call) Run(run func(ctx context.Context, imageRef string)) *MockpushImageOps_Exists_Call {
136+ _c.Call.Run(func(args mock.Arguments) {
137+ var arg0 context.Context
138+ if args[0] != nil {
139+ arg0 = args[0].(context.Context)
140+ }
141+ var arg1 string
142+ if args[1] != nil {
143+ arg1 = args[1].(string)
144+ }
145+ run(
146+ arg0,
147+ arg1,
148+ )
149+ })
150+ return _c
151+}
152+
153+func (_c *MockpushImageOps_Exists_Call) Return(b bool, err error) *MockpushImageOps_Exists_Call {
154+ _c.Call.Return(b, err)
155+ return _c
156+}
157+
158+func (_c *MockpushImageOps_Exists_Call) RunAndReturn(run func(ctx context.Context, imageRef string) (bool, error)) *MockpushImageOps_Exists_Call {
159+ _c.Call.Return(run)
160+ return _c
161+}
162+
163+// Push provides a mock function for the type MockpushImageOps
164+func (_mock *MockpushImageOps) Push(ctx context.Context, ref string) error {
165+ ret := _mock.Called(ctx, ref)
166+
167+ if len(ret) == 0 {
168+ panic("no return value specified for Push")
169+ }
170+
171+ var r0 error
172+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
173+ r0 = returnFunc(ctx, ref)
174+ } else {
175+ r0 = ret.Error(0)
176+ }
177+ return r0
178+}
179+
180+// MockpushImageOps_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push'
181+type MockpushImageOps_Push_Call struct {
182+ *mock.Call
183+}
184+
185+// Push is a helper method to define mock.On call
186+// - ctx context.Context
187+// - ref string
188+func (_e *MockpushImageOps_Expecter) Push(ctx interface{}, ref interface{}) *MockpushImageOps_Push_Call {
189+ return &MockpushImageOps_Push_Call{Call: _e.mock.On("Push", ctx, ref)}
190+}
191+
192+func (_c *MockpushImageOps_Push_Call) Run(run func(ctx context.Context, ref string)) *MockpushImageOps_Push_Call {
193+ _c.Call.Run(func(args mock.Arguments) {
194+ var arg0 context.Context
195+ if args[0] != nil {
196+ arg0 = args[0].(context.Context)
197+ }
198+ var arg1 string
199+ if args[1] != nil {
200+ arg1 = args[1].(string)
201+ }
202+ run(
203+ arg0,
204+ arg1,
205+ )
206+ })
207+ return _c
208+}
209+
210+func (_c *MockpushImageOps_Push_Call) Return(err error) *MockpushImageOps_Push_Call {
211+ _c.Call.Return(err)
212+ return _c
213+}
214+
215+func (_c *MockpushImageOps_Push_Call) RunAndReturn(run func(ctx context.Context, ref string) error) *MockpushImageOps_Push_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
220+// Tag provides a mock function for the type MockpushImageOps
221+func (_mock *MockpushImageOps) Tag(ctx context.Context, sourceRef string, targetRef string) error {
222+ ret := _mock.Called(ctx, sourceRef, targetRef)
223+
224+ if len(ret) == 0 {
225+ panic("no return value specified for Tag")
226+ }
227+
228+ var r0 error
229+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
230+ r0 = returnFunc(ctx, sourceRef, targetRef)
231+ } else {
232+ r0 = ret.Error(0)
233+ }
234+ return r0
235+}
236+
237+// MockpushImageOps_Tag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Tag'
238+type MockpushImageOps_Tag_Call struct {
239+ *mock.Call
240+}
241+
242+// Tag is a helper method to define mock.On call
243+// - ctx context.Context
244+// - sourceRef string
245+// - targetRef string
246+func (_e *MockpushImageOps_Expecter) Tag(ctx interface{}, sourceRef interface{}, targetRef interface{}) *MockpushImageOps_Tag_Call {
247+ return &MockpushImageOps_Tag_Call{Call: _e.mock.On("Tag", ctx, sourceRef, targetRef)}
248+}
249+
250+func (_c *MockpushImageOps_Tag_Call) Run(run func(ctx context.Context, sourceRef string, targetRef string)) *MockpushImageOps_Tag_Call {
251+ _c.Call.Run(func(args mock.Arguments) {
252+ var arg0 context.Context
253+ if args[0] != nil {
254+ arg0 = args[0].(context.Context)
255+ }
256+ var arg1 string
257+ if args[1] != nil {
258+ arg1 = args[1].(string)
259+ }
260+ var arg2 string
261+ if args[2] != nil {
262+ arg2 = args[2].(string)
263+ }
264+ run(
265+ arg0,
266+ arg1,
267+ arg2,
268+ )
269+ })
270+ return _c
271+}
272+
273+func (_c *MockpushImageOps_Tag_Call) Return(err error) *MockpushImageOps_Tag_Call {
274+ _c.Call.Return(err)
275+ return _c
276+}
277+
278+func (_c *MockpushImageOps_Tag_Call) RunAndReturn(run func(ctx context.Context, sourceRef string, targetRef string) error) *MockpushImageOps_Tag_Call {
279+ _c.Call.Return(run)
280+ return _c
281+}
@@ -0,0 +1,281 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewMockpushImageOps creates a new instance of MockpushImageOps. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewMockpushImageOps(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *MockpushImageOps {
19+ mock := &MockpushImageOps{}
20+ mock.Mock.Test(t)
21+
22+ t.Cleanup(func() { mock.AssertExpectations(t) })
23+
24+ return mock
25+}
26+
27+// MockpushImageOps is an autogenerated mock type for the pushImageOps type
28+type MockpushImageOps struct {
29+ mock.Mock
30+}
31+
32+type MockpushImageOps_Expecter struct {
33+ mock *mock.Mock
34+}
35+
36+func (_m *MockpushImageOps) EXPECT() *MockpushImageOps_Expecter {
37+ return &MockpushImageOps_Expecter{mock: &_m.Mock}
38+}
39+
40+// Build provides a mock function for the type MockpushImageOps
41+func (_mock *MockpushImageOps) Build(ctx context.Context, args []string) error {
42+ ret := _mock.Called(ctx, args)
43+
44+ if len(ret) == 0 {
45+ panic("no return value specified for Build")
46+ }
47+
48+ var r0 error
49+ if returnFunc, ok := ret.Get(0).(func(context.Context, []string) error); ok {
50+ r0 = returnFunc(ctx, args)
51+ } else {
52+ r0 = ret.Error(0)
53+ }
54+ return r0
55+}
56+
57+// MockpushImageOps_Build_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Build'
58+type MockpushImageOps_Build_Call struct {
59+ *mock.Call
60+}
61+
62+// Build is a helper method to define mock.On call
63+// - ctx context.Context
64+// - args []string
65+func (_e *MockpushImageOps_Expecter) Build(ctx interface{}, args interface{}) *MockpushImageOps_Build_Call {
66+ return &MockpushImageOps_Build_Call{Call: _e.mock.On("Build", ctx, args)}
67+}
68+
69+func (_c *MockpushImageOps_Build_Call) Run(run func(ctx context.Context, args []string)) *MockpushImageOps_Build_Call {
70+ _c.Call.Run(func(args mock.Arguments) {
71+ var arg0 context.Context
72+ if args[0] != nil {
73+ arg0 = args[0].(context.Context)
74+ }
75+ var arg1 []string
76+ if args[1] != nil {
77+ arg1 = args[1].([]string)
78+ }
79+ run(
80+ arg0,
81+ arg1,
82+ )
83+ })
84+ return _c
85+}
86+
87+func (_c *MockpushImageOps_Build_Call) Return(err error) *MockpushImageOps_Build_Call {
88+ _c.Call.Return(err)
89+ return _c
90+}
91+
92+func (_c *MockpushImageOps_Build_Call) RunAndReturn(run func(ctx context.Context, args []string) error) *MockpushImageOps_Build_Call {
93+ _c.Call.Return(run)
94+ return _c
95+}
96+
97+// Exists provides a mock function for the type MockpushImageOps
98+func (_mock *MockpushImageOps) Exists(ctx context.Context, imageRef string) (bool, error) {
99+ ret := _mock.Called(ctx, imageRef)
100+
101+ if len(ret) == 0 {
102+ panic("no return value specified for Exists")
103+ }
104+
105+ var r0 bool
106+ var r1 error
107+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
108+ return returnFunc(ctx, imageRef)
109+ }
110+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok {
111+ r0 = returnFunc(ctx, imageRef)
112+ } else {
113+ r0 = ret.Get(0).(bool)
114+ }
115+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
116+ r1 = returnFunc(ctx, imageRef)
117+ } else {
118+ r1 = ret.Error(1)
119+ }
120+ return r0, r1
121+}
122+
123+// MockpushImageOps_Exists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exists'
124+type MockpushImageOps_Exists_Call struct {
125+ *mock.Call
126+}
127+
128+// Exists is a helper method to define mock.On call
129+// - ctx context.Context
130+// - imageRef string
131+func (_e *MockpushImageOps_Expecter) Exists(ctx interface{}, imageRef interface{}) *MockpushImageOps_Exists_Call {
132+ return &MockpushImageOps_Exists_Call{Call: _e.mock.On("Exists", ctx, imageRef)}
133+}
134+
135+func (_c *MockpushImageOps_Exists_Call) Run(run func(ctx context.Context, imageRef string)) *MockpushImageOps_Exists_Call {
136+ _c.Call.Run(func(args mock.Arguments) {
137+ var arg0 context.Context
138+ if args[0] != nil {
139+ arg0 = args[0].(context.Context)
140+ }
141+ var arg1 string
142+ if args[1] != nil {
143+ arg1 = args[1].(string)
144+ }
145+ run(
146+ arg0,
147+ arg1,
148+ )
149+ })
150+ return _c
151+}
152+
153+func (_c *MockpushImageOps_Exists_Call) Return(b bool, err error) *MockpushImageOps_Exists_Call {
154+ _c.Call.Return(b, err)
155+ return _c
156+}
157+
158+func (_c *MockpushImageOps_Exists_Call) RunAndReturn(run func(ctx context.Context, imageRef string) (bool, error)) *MockpushImageOps_Exists_Call {
159+ _c.Call.Return(run)
160+ return _c
161+}
162+
163+// Push provides a mock function for the type MockpushImageOps
164+func (_mock *MockpushImageOps) Push(ctx context.Context, ref string) error {
165+ ret := _mock.Called(ctx, ref)
166+
167+ if len(ret) == 0 {
168+ panic("no return value specified for Push")
169+ }
170+
171+ var r0 error
172+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
173+ r0 = returnFunc(ctx, ref)
174+ } else {
175+ r0 = ret.Error(0)
176+ }
177+ return r0
178+}
179+
180+// MockpushImageOps_Push_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Push'
181+type MockpushImageOps_Push_Call struct {
182+ *mock.Call
183+}
184+
185+// Push is a helper method to define mock.On call
186+// - ctx context.Context
187+// - ref string
188+func (_e *MockpushImageOps_Expecter) Push(ctx interface{}, ref interface{}) *MockpushImageOps_Push_Call {
189+ return &MockpushImageOps_Push_Call{Call: _e.mock.On("Push", ctx, ref)}
190+}
191+
192+func (_c *MockpushImageOps_Push_Call) Run(run func(ctx context.Context, ref string)) *MockpushImageOps_Push_Call {
193+ _c.Call.Run(func(args mock.Arguments) {
194+ var arg0 context.Context
195+ if args[0] != nil {
196+ arg0 = args[0].(context.Context)
197+ }
198+ var arg1 string
199+ if args[1] != nil {
200+ arg1 = args[1].(string)
201+ }
202+ run(
203+ arg0,
204+ arg1,
205+ )
206+ })
207+ return _c
208+}
209+
210+func (_c *MockpushImageOps_Push_Call) Return(err error) *MockpushImageOps_Push_Call {
211+ _c.Call.Return(err)
212+ return _c
213+}
214+
215+func (_c *MockpushImageOps_Push_Call) RunAndReturn(run func(ctx context.Context, ref string) error) *MockpushImageOps_Push_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
220+// Tag provides a mock function for the type MockpushImageOps
221+func (_mock *MockpushImageOps) Tag(ctx context.Context, sourceRef string, targetRef string) error {
222+ ret := _mock.Called(ctx, sourceRef, targetRef)
223+
224+ if len(ret) == 0 {
225+ panic("no return value specified for Tag")
226+ }
227+
228+ var r0 error
229+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
230+ r0 = returnFunc(ctx, sourceRef, targetRef)
231+ } else {
232+ r0 = ret.Error(0)
233+ }
234+ return r0
235+}
236+
237+// MockpushImageOps_Tag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Tag'
238+type MockpushImageOps_Tag_Call struct {
239+ *mock.Call
240+}
241+
242+// Tag is a helper method to define mock.On call
243+// - ctx context.Context
244+// - sourceRef string
245+// - targetRef string
246+func (_e *MockpushImageOps_Expecter) Tag(ctx interface{}, sourceRef interface{}, targetRef interface{}) *MockpushImageOps_Tag_Call {
247+ return &MockpushImageOps_Tag_Call{Call: _e.mock.On("Tag", ctx, sourceRef, targetRef)}
248+}
249+
250+func (_c *MockpushImageOps_Tag_Call) Run(run func(ctx context.Context, sourceRef string, targetRef string)) *MockpushImageOps_Tag_Call {
251+ _c.Call.Run(func(args mock.Arguments) {
252+ var arg0 context.Context
253+ if args[0] != nil {
254+ arg0 = args[0].(context.Context)
255+ }
256+ var arg1 string
257+ if args[1] != nil {
258+ arg1 = args[1].(string)
259+ }
260+ var arg2 string
261+ if args[2] != nil {
262+ arg2 = args[2].(string)
263+ }
264+ run(
265+ arg0,
266+ arg1,
267+ arg2,
268+ )
269+ })
270+ return _c
271+}
272+
273+func (_c *MockpushImageOps_Tag_Call) Return(err error) *MockpushImageOps_Tag_Call {
274+ _c.Call.Return(err)
275+ return _c
276+}
277+
278+func (_c *MockpushImageOps_Tag_Call) RunAndReturn(run func(ctx context.Context, sourceRef string, targetRef string) error) *MockpushImageOps_Tag_Call {
279+ _c.Call.Return(run)
280+ return _c
281+}
M internal/adapters/in/cli/push.go +23 -44
@@ -18,15 +18,10 @@ import (
1818 "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
1919 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
2020 "github.com/bnema/gordon/internal/domain"
21- "github.com/bnema/gordon/pkg/registrypush"
2221 "github.com/bnema/gordon/pkg/validation"
2322 )
2423
25-var (
26- dockerTagFn = dockerTag
27- dockerPushFn = dockerPush
28- dockerImageExistsFn = dockerImageExists
29-)
24+var newImageOpsFn = newImageOpsFromFlags
3025
3126 func newPushCmd() *cobra.Command {
3227 var (
@@ -155,6 +150,11 @@ func runPush(ctx context.Context, imageArg, domainFlag, tag string, build bool,
155150
156151 versionRef, latestRef := resolveImageRefs(registry, imageName, version)
157152
153+ imageOps, err := newImageOpsFn()
154+ if err != nil {
155+ return err
156+ }
157+
158158 fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(versionRef))
159159 if version != "latest" {
160160 fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(latestRef))
@@ -171,11 +171,11 @@ func runPush(ctx context.Context, imageArg, domainFlag, tag string, build bool,
171171 }
172172
173173 if build {
174- if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {
174+ if err := buildAndPush(ctx, imageOps, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {
175175 return err
176176 }
177177 } else {
178- if err := tagAndPush(ctx, registry, imageName, version, versionRef, latestRef); err != nil {
178+ if err := tagAndPush(ctx, imageOps, registry, imageName, version, versionRef, latestRef); err != nil {
179179 return err
180180 }
181181 }
@@ -474,7 +474,7 @@ func parseTagRef(ref string) string {
474474 return tag
475475 }
476476
477-func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {
477+func buildAndPush(ctx context.Context, ops pushImageOps, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {
478478 if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
479479 return fmt.Errorf("dockerfile not found: %s", dockerfile)
480480 }
@@ -483,20 +483,16 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui
483483 // The native registry push client handles chunked uploads to stay
484484 // within Cloudflare's 100MB per-request limit.
485485 fmt.Println("\nBuilding image...")
486- buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204
487- buildCmd.Env = os.Environ() // VERSION is now passed as --build-arg VERSION=<value>
488- buildCmd.Stdout = os.Stdout
489- buildCmd.Stderr = os.Stderr
490- if err := buildCmd.Run(); err != nil {
491- return fmt.Errorf("docker buildx build failed: %w", err)
486+ if err := ops.Build(ctx, buildImageArgs(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)); err != nil {
487+ return err
492488 }
493489
494490 fmt.Println("Pushing...")
495- if err := dockerPushFn(ctx, latestRef); err != nil {
491+ if err := ops.Push(ctx, latestRef); err != nil {
496492 return fmt.Errorf("failed to push %s: %w", latestRef, err)
497493 }
498494 if version != "latest" {
499- if err := dockerPushFn(ctx, versionRef); err != nil {
495+ if err := ops.Push(ctx, versionRef); err != nil {
500496 return fmt.Errorf("failed to push %s: %w", versionRef, err)
501497 }
502498 }
@@ -554,30 +550,34 @@ func buildImageArgs(ctx context.Context, version, platform, dockerfile string, b
554550 return args
555551 }
556552
557-func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error {
553+func tagAndPush(ctx context.Context, ops pushImageOps, registry, imageName, version, versionRef, latestRef string) error {
558554 localImage := fmt.Sprintf("%s/%s", registry, imageName)
559555
560556 fmt.Println("\nChecking local image...")
561- if !dockerImageExistsFn(ctx, localImage) {
557+ exists, err := ops.Exists(ctx, localImage)
558+ if err != nil {
559+ return fmt.Errorf("failed to inspect local image %s: %w", localImage, err)
560+ }
561+ if !exists {
562562 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)
563563 }
564564
565565 fmt.Println("Tagging...")
566- if err := dockerTagFn(ctx, localImage, versionRef); err != nil {
566+ if err := ops.Tag(ctx, localImage, versionRef); err != nil {
567567 return fmt.Errorf("failed to tag %s: %w", versionRef, err)
568568 }
569569 if version != "latest" {
570- if err := dockerTagFn(ctx, localImage, latestRef); err != nil {
570+ if err := ops.Tag(ctx, localImage, latestRef); err != nil {
571571 return fmt.Errorf("failed to tag %s: %w", latestRef, err)
572572 }
573573 }
574574
575575 fmt.Println("Pushing...")
576- if err := dockerPushFn(ctx, versionRef); err != nil {
576+ if err := ops.Push(ctx, versionRef); err != nil {
577577 return fmt.Errorf("failed to push %s: %w", versionRef, err)
578578 }
579579 if version != "latest" {
580- if err := dockerPushFn(ctx, latestRef); err != nil {
580+ if err := ops.Push(ctx, latestRef); err != nil {
581581 return fmt.Errorf("failed to push %s: %w", latestRef, err)
582582 }
583583 }
@@ -742,27 +742,6 @@ func getGitVersion(ctx context.Context) string {
742742 return strings.TrimSpace(string(out))
743743 }
744744
745-func dockerTag(ctx context.Context, src, dst string) error {
746- cmd := exec.CommandContext(ctx, "docker", "tag", src, dst) //nolint:gosec // binary is constant; image refs validated by OCI ref parser
747- cmd.Stdout = os.Stdout
748- cmd.Stderr = os.Stderr
749- return cmd.Run()
750-}
751-
752-func dockerPush(ctx context.Context, ref string) error {
753- pusher := registrypush.New(
754- registrypush.WithProgress(os.Stderr),
755- )
756- return pusher.Push(ctx, ref)
757-}
758-
759-func dockerImageExists(ctx context.Context, image string) bool {
760- cmd := exec.CommandContext(ctx, "docker", "image", "inspect", image) // #nosec G204
761- cmd.Stdout = nil
762- cmd.Stderr = nil
763- return cmd.Run() == nil
764-}
765-
766745 // buildArgPattern matches KEY=VALUE where KEY starts with a letter or underscore.
767746 var buildArgPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*$`)
768747
@@ -18,15 +18,10 @@ import (
18 "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"18 "github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
19 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"19 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
20 "github.com/bnema/gordon/internal/domain"20 "github.com/bnema/gordon/internal/domain"
21- "github.com/bnema/gordon/pkg/registrypush"
22 "github.com/bnema/gordon/pkg/validation"21 "github.com/bnema/gordon/pkg/validation"
23 )22 )
24 23
25-var (24+var newImageOpsFn = newImageOpsFromFlags
26- dockerTagFn = dockerTag
27- dockerPushFn = dockerPush
28- dockerImageExistsFn = dockerImageExists
29-)
30 25
31 func newPushCmd() *cobra.Command {26 func newPushCmd() *cobra.Command {
32 var (27 var (
@@ -155,6 +150,11 @@ func runPush(ctx context.Context, imageArg, domainFlag, tag string, build bool,
155 150
156 versionRef, latestRef := resolveImageRefs(registry, imageName, version)151 versionRef, latestRef := resolveImageRefs(registry, imageName, version)
157 152
153+ imageOps, err := newImageOpsFn()
154+ if err != nil {
155+ return err
156+ }
157+
158 fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(versionRef))158 fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(versionRef))
159 if version != "latest" {159 if version != "latest" {
160 fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(latestRef))160 fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(latestRef))
@@ -171,11 +171,11 @@ func runPush(ctx context.Context, imageArg, domainFlag, tag string, build bool,
171 }171 }
172 172
173 if build {173 if build {
174- if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {174+ if err := buildAndPush(ctx, imageOps, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {
175 return err175 return err
176 }176 }
177 } else {177 } else {
178- if err := tagAndPush(ctx, registry, imageName, version, versionRef, latestRef); err != nil {178+ if err := tagAndPush(ctx, imageOps, registry, imageName, version, versionRef, latestRef); err != nil {
179 return err179 return err
180 }180 }
181 }181 }
@@ -474,7 +474,7 @@ func parseTagRef(ref string) string {
474 return tag474 return tag
475 }475 }
476 476
477-func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {477+func buildAndPush(ctx context.Context, ops pushImageOps, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {
478 if _, err := os.Stat(dockerfile); os.IsNotExist(err) {478 if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
479 return fmt.Errorf("dockerfile not found: %s", dockerfile)479 return fmt.Errorf("dockerfile not found: %s", dockerfile)
480 }480 }
@@ -483,20 +483,16 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui
483 // The native registry push client handles chunked uploads to stay483 // The native registry push client handles chunked uploads to stay
484 // within Cloudflare's 100MB per-request limit.484 // within Cloudflare's 100MB per-request limit.
485 fmt.Println("\nBuilding image...")485 fmt.Println("\nBuilding image...")
486- buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204486+ if err := ops.Build(ctx, buildImageArgs(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)); err != nil {
487- buildCmd.Env = os.Environ() // VERSION is now passed as --build-arg VERSION=<value>487+ return err
488- buildCmd.Stdout = os.Stdout
489- buildCmd.Stderr = os.Stderr
490- if err := buildCmd.Run(); err != nil {
491- return fmt.Errorf("docker buildx build failed: %w", err)
492 }488 }
493 489
494 fmt.Println("Pushing...")490 fmt.Println("Pushing...")
495- if err := dockerPushFn(ctx, latestRef); err != nil {491+ if err := ops.Push(ctx, latestRef); err != nil {
496 return fmt.Errorf("failed to push %s: %w", latestRef, err)492 return fmt.Errorf("failed to push %s: %w", latestRef, err)
497 }493 }
498 if version != "latest" {494 if version != "latest" {
499- if err := dockerPushFn(ctx, versionRef); err != nil {495+ if err := ops.Push(ctx, versionRef); err != nil {
500 return fmt.Errorf("failed to push %s: %w", versionRef, err)496 return fmt.Errorf("failed to push %s: %w", versionRef, err)
501 }497 }
502 }498 }
@@ -554,30 +550,34 @@ func buildImageArgs(ctx context.Context, version, platform, dockerfile string, b
554 return args550 return args
555 }551 }
556 552
557-func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error {553+func tagAndPush(ctx context.Context, ops pushImageOps, registry, imageName, version, versionRef, latestRef string) error {
558 localImage := fmt.Sprintf("%s/%s", registry, imageName)554 localImage := fmt.Sprintf("%s/%s", registry, imageName)
559 555
560 fmt.Println("\nChecking local image...")556 fmt.Println("\nChecking local image...")
561- if !dockerImageExistsFn(ctx, localImage) {557+ exists, err := ops.Exists(ctx, localImage)
558+ if err != nil {
559+ return fmt.Errorf("failed to inspect local image %s: %w", localImage, err)
560+ }
561+ if !exists {
562 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)562 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)
563 }563 }
564 564
565 fmt.Println("Tagging...")565 fmt.Println("Tagging...")
566- if err := dockerTagFn(ctx, localImage, versionRef); err != nil {566+ if err := ops.Tag(ctx, localImage, versionRef); err != nil {
567 return fmt.Errorf("failed to tag %s: %w", versionRef, err)567 return fmt.Errorf("failed to tag %s: %w", versionRef, err)
568 }568 }
569 if version != "latest" {569 if version != "latest" {
570- if err := dockerTagFn(ctx, localImage, latestRef); err != nil {570+ if err := ops.Tag(ctx, localImage, latestRef); err != nil {
571 return fmt.Errorf("failed to tag %s: %w", latestRef, err)571 return fmt.Errorf("failed to tag %s: %w", latestRef, err)
572 }572 }
573 }573 }
574 574
575 fmt.Println("Pushing...")575 fmt.Println("Pushing...")
576- if err := dockerPushFn(ctx, versionRef); err != nil {576+ if err := ops.Push(ctx, versionRef); err != nil {
577 return fmt.Errorf("failed to push %s: %w", versionRef, err)577 return fmt.Errorf("failed to push %s: %w", versionRef, err)
578 }578 }
579 if version != "latest" {579 if version != "latest" {
580- if err := dockerPushFn(ctx, latestRef); err != nil {580+ if err := ops.Push(ctx, latestRef); err != nil {
581 return fmt.Errorf("failed to push %s: %w", latestRef, err)581 return fmt.Errorf("failed to push %s: %w", latestRef, err)
582 }582 }
583 }583 }
@@ -742,27 +742,6 @@ func getGitVersion(ctx context.Context) string {
742 return strings.TrimSpace(string(out))742 return strings.TrimSpace(string(out))
743 }743 }
744 744
745-func dockerTag(ctx context.Context, src, dst string) error {
746- cmd := exec.CommandContext(ctx, "docker", "tag", src, dst) //nolint:gosec // binary is constant; image refs validated by OCI ref parser
747- cmd.Stdout = os.Stdout
748- cmd.Stderr = os.Stderr
749- return cmd.Run()
750-}
751-
752-func dockerPush(ctx context.Context, ref string) error {
753- pusher := registrypush.New(
754- registrypush.WithProgress(os.Stderr),
755- )
756- return pusher.Push(ctx, ref)
757-}
758-
759-func dockerImageExists(ctx context.Context, image string) bool {
760- cmd := exec.CommandContext(ctx, "docker", "image", "inspect", image) // #nosec G204
761- cmd.Stdout = nil
762- cmd.Stderr = nil
763- return cmd.Run() == nil
764-}
765-
766 // buildArgPattern matches KEY=VALUE where KEY starts with a letter or underscore.745 // buildArgPattern matches KEY=VALUE where KEY starts with a letter or underscore.
767 var buildArgPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*$`)746 var buildArgPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*$`)
768 747
A internal/adapters/in/cli/push_image_ops.go +101 -0
@@ -0,0 +1,101 @@
1+package cli
2+
3+import (
4+ "context"
5+ "fmt"
6+ "io"
7+ "os"
8+ "os/exec"
9+
10+ cerrdefs "github.com/containerd/errdefs"
11+ "github.com/docker/docker/client"
12+
13+ "github.com/bnema/gordon/internal/adapters/in/cli/remote"
14+ "github.com/bnema/gordon/pkg/registrypush"
15+)
16+
17+// pushImageOps abstracts container image operations needed by push commands.
18+// The real implementation uses Docker SDK + registrypush; tests inject fakes.
19+type pushImageOps interface {
20+ // Tag creates a new tag for an existing local image.
21+ Tag(ctx context.Context, sourceRef, targetRef string) error
22+ // Push uploads a local image to its registry using chunked blob uploads.
23+ Push(ctx context.Context, ref string) error
24+ // Exists checks whether a local image exists.
25+ Exists(ctx context.Context, imageRef string) (bool, error)
26+ // Build runs a container image build. Only Docker with buildx is supported;
27+ // other runtimes return an actionable error.
28+ Build(ctx context.Context, args []string) error
29+}
30+
31+// dockerImageOps implements pushImageOps using the Docker SDK and registrypush.
32+type dockerImageOps struct {
33+ cli client.APIClient
34+ insecureTLS bool
35+ progress io.Writer
36+}
37+
38+// newDockerImageOps creates a dockerImageOps from the Docker environment.
39+func newDockerImageOps(insecureTLS bool, progress io.Writer) (*dockerImageOps, error) {
40+ cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
41+ if err != nil {
42+ return nil, fmt.Errorf("failed to create Docker client: %w", err)
43+ }
44+ return &dockerImageOps{
45+ cli: cli,
46+ insecureTLS: insecureTLS,
47+ progress: progress,
48+ }, nil
49+}
50+
51+// newImageOpsFromFlags resolves TLS settings from CLI flags/config and creates ops.
52+func newImageOpsFromFlags() (pushImageOps, error) {
53+ _, _, insecureTLS, _ := remote.ResolveRemote(remoteFlag, tokenFlag, insecureTLSFlag)
54+ return newDockerImageOps(insecureTLS, os.Stderr)
55+}
56+
57+func (d *dockerImageOps) Tag(ctx context.Context, sourceRef, targetRef string) error {
58+ if err := d.cli.ImageTag(ctx, sourceRef, targetRef); err != nil {
59+ return fmt.Errorf("failed to tag %s as %s: %w", sourceRef, targetRef, err)
60+ }
61+ return nil
62+}
63+
64+func (d *dockerImageOps) Push(ctx context.Context, ref string) error {
65+ pusher := registrypush.New(
66+ registrypush.WithProgress(d.progress),
67+ registrypush.WithInsecureTLS(d.insecureTLS),
68+ )
69+ return pusher.Push(ctx, ref)
70+}
71+
72+func (d *dockerImageOps) Exists(ctx context.Context, imageRef string) (bool, error) {
73+ _, err := d.cli.ImageInspect(ctx, imageRef)
74+ if err != nil {
75+ if cerrdefs.IsNotFound(err) {
76+ return false, nil
77+ }
78+ return false, fmt.Errorf("failed to inspect image %s: %w", imageRef, err)
79+ }
80+ return true, nil
81+}
82+
83+func (d *dockerImageOps) Build(ctx context.Context, args []string) error {
84+ if _, err := exec.LookPath("docker"); err != nil {
85+ return fmt.Errorf(
86+ "image build requires Docker with buildx\n\n" +
87+ "Podman and other runtimes are not supported for --build.\n" +
88+ "Build the image manually and push with:\n" +
89+ " podman build -t <image> .\n" +
90+ " gordon push <image>",
91+ )
92+ }
93+ cmd := exec.CommandContext(ctx, "docker", args...) // #nosec G204
94+ cmd.Env = os.Environ()
95+ cmd.Stdout = os.Stdout
96+ cmd.Stderr = os.Stderr
97+ if err := cmd.Run(); err != nil {
98+ return fmt.Errorf("docker buildx build failed: %w", err)
99+ }
100+ return nil
101+}
@@ -0,0 +1,101 @@
1+package cli
2+
3+import (
4+ "context"
5+ "fmt"
6+ "io"
7+ "os"
8+ "os/exec"
9+
10+ cerrdefs "github.com/containerd/errdefs"
11+ "github.com/docker/docker/client"
12+
13+ "github.com/bnema/gordon/internal/adapters/in/cli/remote"
14+ "github.com/bnema/gordon/pkg/registrypush"
15+)
16+
17+// pushImageOps abstracts container image operations needed by push commands.
18+// The real implementation uses Docker SDK + registrypush; tests inject fakes.
19+type pushImageOps interface {
20+ // Tag creates a new tag for an existing local image.
21+ Tag(ctx context.Context, sourceRef, targetRef string) error
22+ // Push uploads a local image to its registry using chunked blob uploads.
23+ Push(ctx context.Context, ref string) error
24+ // Exists checks whether a local image exists.
25+ Exists(ctx context.Context, imageRef string) (bool, error)
26+ // Build runs a container image build. Only Docker with buildx is supported;
27+ // other runtimes return an actionable error.
28+ Build(ctx context.Context, args []string) error
29+}
30+
31+// dockerImageOps implements pushImageOps using the Docker SDK and registrypush.
32+type dockerImageOps struct {
33+ cli client.APIClient
34+ insecureTLS bool
35+ progress io.Writer
36+}
37+
38+// newDockerImageOps creates a dockerImageOps from the Docker environment.
39+func newDockerImageOps(insecureTLS bool, progress io.Writer) (*dockerImageOps, error) {
40+ cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
41+ if err != nil {
42+ return nil, fmt.Errorf("failed to create Docker client: %w", err)
43+ }
44+ return &dockerImageOps{
45+ cli: cli,
46+ insecureTLS: insecureTLS,
47+ progress: progress,
48+ }, nil
49+}
50+
51+// newImageOpsFromFlags resolves TLS settings from CLI flags/config and creates ops.
52+func newImageOpsFromFlags() (pushImageOps, error) {
53+ _, _, insecureTLS, _ := remote.ResolveRemote(remoteFlag, tokenFlag, insecureTLSFlag)
54+ return newDockerImageOps(insecureTLS, os.Stderr)
55+}
56+
57+func (d *dockerImageOps) Tag(ctx context.Context, sourceRef, targetRef string) error {
58+ if err := d.cli.ImageTag(ctx, sourceRef, targetRef); err != nil {
59+ return fmt.Errorf("failed to tag %s as %s: %w", sourceRef, targetRef, err)
60+ }
61+ return nil
62+}
63+
64+func (d *dockerImageOps) Push(ctx context.Context, ref string) error {
65+ pusher := registrypush.New(
66+ registrypush.WithProgress(d.progress),
67+ registrypush.WithInsecureTLS(d.insecureTLS),
68+ )
69+ return pusher.Push(ctx, ref)
70+}
71+
72+func (d *dockerImageOps) Exists(ctx context.Context, imageRef string) (bool, error) {
73+ _, err := d.cli.ImageInspect(ctx, imageRef)
74+ if err != nil {
75+ if cerrdefs.IsNotFound(err) {
76+ return false, nil
77+ }
78+ return false, fmt.Errorf("failed to inspect image %s: %w", imageRef, err)
79+ }
80+ return true, nil
81+}
82+
83+func (d *dockerImageOps) Build(ctx context.Context, args []string) error {
84+ if _, err := exec.LookPath("docker"); err != nil {
85+ return fmt.Errorf(
86+ "image build requires Docker with buildx\n\n" +
87+ "Podman and other runtimes are not supported for --build.\n" +
88+ "Build the image manually and push with:\n" +
89+ " podman build -t <image> .\n" +
90+ " gordon push <image>",
91+ )
92+ }
93+ cmd := exec.CommandContext(ctx, "docker", args...) // #nosec G204
94+ cmd.Env = os.Environ()
95+ cmd.Stdout = os.Stdout
96+ cmd.Stderr = os.Stderr
97+ if err := cmd.Run(); err != nil {
98+ return fmt.Errorf("docker buildx build failed: %w", err)
99+ }
100+ return nil
101+}
M internal/adapters/in/cli/push_integration_test.go +8 -18
@@ -8,10 +8,12 @@ import (
88 "strings"
99 "testing"
1010
11+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
1112 "github.com/bnema/zerowrap"
1213 v1 "github.com/google/go-containerregistry/pkg/v1"
1314 "github.com/google/go-containerregistry/pkg/v1/random"
1415 "github.com/stretchr/testify/assert"
16+ "github.com/stretchr/testify/mock"
1517 "github.com/stretchr/testify/require"
1618
1719 registryhttp "github.com/bnema/gordon/internal/adapters/in/http/registry"
@@ -31,16 +33,10 @@ func TestIntegration_TagAndPush_NativeRegistry(t *testing.T) {
3133 img, err := random.Image(512, 1)
3234 require.NoError(t, err)
3335
34- originalPush := dockerPushFn
35- originalTag := dockerTagFn
36- originalImageExists := dockerImageExistsFn
37- t.Cleanup(func() {
38- dockerPushFn = originalPush
39- dockerTagFn = originalTag
40- dockerImageExistsFn = originalImageExists
41- })
42-
43- dockerPushFn = func(ctx context.Context, ref string) error {
36+ ops := climocks.NewMockpushImageOps(t)
37+ ops.On("Tag", mock.Anything, mock.Anything, mock.Anything).Return(nil)
38+ ops.On("Exists", mock.Anything, mock.Anything).Return(true, nil)
39+ ops.EXPECT().Push(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, ref string) error {
4440 pusher := registrypush.New(
4541 registrypush.WithChunkSize(256),
4642 registrypush.WithTransport(harness.server.Client().Transport),
@@ -49,19 +45,13 @@ func TestIntegration_TagAndPush_NativeRegistry(t *testing.T) {
4945 }),
5046 )
5147 return pusher.Push(ctx, ref)
52- }
53- dockerTagFn = func(context.Context, string, string) error {
54- return nil
55- }
56- dockerImageExistsFn = func(context.Context, string) bool {
57- return true
58- }
48+ })
5949
6050 registry := strings.TrimPrefix(harness.server.URL, "http://")
6151 versionRef := registry + "/testapp:v1.0.0"
6252 latestRef := registry + "/testapp:latest"
6353
64- err = tagAndPush(ctx, registry, "testapp", "v1.0.0", versionRef, latestRef)
54+ err = tagAndPush(ctx, ops, registry, "testapp", "v1.0.0", versionRef, latestRef)
6555 require.NoError(t, err)
6656
6757 harness.RequireManifest(t, "v1.0.0")
@@ -8,10 +8,12 @@ import (
8 "strings"8 "strings"
9 "testing"9 "testing"
10 10
11+ climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
11 "github.com/bnema/zerowrap"12 "github.com/bnema/zerowrap"
12 v1 "github.com/google/go-containerregistry/pkg/v1"13 v1 "github.com/google/go-containerregistry/pkg/v1"
13 "github.com/google/go-containerregistry/pkg/v1/random"14 "github.com/google/go-containerregistry/pkg/v1/random"
14 "github.com/stretchr/testify/assert"15 "github.com/stretchr/testify/assert"
16+ "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"17 "github.com/stretchr/testify/require"
16 18
17 registryhttp "github.com/bnema/gordon/internal/adapters/in/http/registry"19 registryhttp "github.com/bnema/gordon/internal/adapters/in/http/registry"
@@ -31,16 +33,10 @@ func TestIntegration_TagAndPush_NativeRegistry(t *testing.T) {
31 img, err := random.Image(512, 1)33 img, err := random.Image(512, 1)
32 require.NoError(t, err)34 require.NoError(t, err)
33 35
34- originalPush := dockerPushFn36+ ops := climocks.NewMockpushImageOps(t)
35- originalTag := dockerTagFn37+ ops.On("Tag", mock.Anything, mock.Anything, mock.Anything).Return(nil)
36- originalImageExists := dockerImageExistsFn38+ ops.On("Exists", mock.Anything, mock.Anything).Return(true, nil)
37- t.Cleanup(func() {39+ ops.EXPECT().Push(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, ref string) error {
38- dockerPushFn = originalPush
39- dockerTagFn = originalTag
40- dockerImageExistsFn = originalImageExists
41- })
42-
43- dockerPushFn = func(ctx context.Context, ref string) error {
44 pusher := registrypush.New(40 pusher := registrypush.New(
45 registrypush.WithChunkSize(256),41 registrypush.WithChunkSize(256),
46 registrypush.WithTransport(harness.server.Client().Transport),42 registrypush.WithTransport(harness.server.Client().Transport),
@@ -49,19 +45,13 @@ func TestIntegration_TagAndPush_NativeRegistry(t *testing.T) {
49 }),45 }),
50 )46 )
51 return pusher.Push(ctx, ref)47 return pusher.Push(ctx, ref)
52- }48+ })
53- dockerTagFn = func(context.Context, string, string) error {
54- return nil
55- }
56- dockerImageExistsFn = func(context.Context, string) bool {
57- return true
58- }
59 49
60 registry := strings.TrimPrefix(harness.server.URL, "http://")50 registry := strings.TrimPrefix(harness.server.URL, "http://")
61 versionRef := registry + "/testapp:v1.0.0"51 versionRef := registry + "/testapp:v1.0.0"
62 latestRef := registry + "/testapp:latest"52 latestRef := registry + "/testapp:latest"
63 53
64- err = tagAndPush(ctx, registry, "testapp", "v1.0.0", versionRef, latestRef)54+ err = tagAndPush(ctx, ops, registry, "testapp", "v1.0.0", versionRef, latestRef)
65 require.NoError(t, err)55 require.NoError(t, err)
66 56
67 harness.RequireManifest(t, "v1.0.0")57 harness.RequireManifest(t, "v1.0.0")
M internal/boundaries/in/mocks/mock_config_service.go +30 -30
@@ -158,38 +158,38 @@ func (_c *MockConfigService_AddRoute_Call) RunAndReturn(run func(ctx context.Con
158158 return _c
159159 }
160160
161-// FindRoutesByImage provides a mock function for the type MockConfigService
162-func (_mock *MockConfigService) FindRoutesByImage(ctx context.Context, imageName string) []domain.Route {
161+// FindAttachmentTargetsByImage provides a mock function for the type MockConfigService
162+func (_mock *MockConfigService) FindAttachmentTargetsByImage(ctx context.Context, imageName string) []string {
163163 ret := _mock.Called(ctx, imageName)
164164
165165 if len(ret) == 0 {
166- panic("no return value specified for FindRoutesByImage")
166+ panic("no return value specified for FindAttachmentTargetsByImage")
167167 }
168168
169- var r0 []domain.Route
170- if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Route); ok {
169+ var r0 []string
170+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok {
171171 r0 = returnFunc(ctx, imageName)
172172 } else {
173173 if ret.Get(0) != nil {
174- r0 = ret.Get(0).([]domain.Route)
174+ r0 = ret.Get(0).([]string)
175175 }
176176 }
177177 return r0
178178 }
179179
180-// MockConfigService_FindRoutesByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRoutesByImage'
181-type MockConfigService_FindRoutesByImage_Call struct {
180+// MockConfigService_FindAttachmentTargetsByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindAttachmentTargetsByImage'
181+type MockConfigService_FindAttachmentTargetsByImage_Call struct {
182182 *mock.Call
183183 }
184184
185-// FindRoutesByImage is a helper method to define mock.On call
185+// FindAttachmentTargetsByImage is a helper method to define mock.On call
186186 // - ctx context.Context
187187 // - imageName string
188-func (_e *MockConfigService_Expecter) FindRoutesByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindRoutesByImage_Call {
189- return &MockConfigService_FindRoutesByImage_Call{Call: _e.mock.On("FindRoutesByImage", ctx, imageName)}
188+func (_e *MockConfigService_Expecter) FindAttachmentTargetsByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindAttachmentTargetsByImage_Call {
189+ return &MockConfigService_FindAttachmentTargetsByImage_Call{Call: _e.mock.On("FindAttachmentTargetsByImage", ctx, imageName)}
190190 }
191191
192-func (_c *MockConfigService_FindRoutesByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindRoutesByImage_Call {
192+func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindAttachmentTargetsByImage_Call {
193193 _c.Call.Run(func(args mock.Arguments) {
194194 var arg0 context.Context
195195 if args[0] != nil {
@@ -207,48 +207,48 @@ func (_c *MockConfigService_FindRoutesByImage_Call) Run(run func(ctx context.Con
207207 return _c
208208 }
209209
210-func (_c *MockConfigService_FindRoutesByImage_Call) Return(routes []domain.Route) *MockConfigService_FindRoutesByImage_Call {
211- _c.Call.Return(routes)
210+func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Return(strings []string) *MockConfigService_FindAttachmentTargetsByImage_Call {
211+ _c.Call.Return(strings)
212212 return _c
213213 }
214214
215-func (_c *MockConfigService_FindRoutesByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []domain.Route) *MockConfigService_FindRoutesByImage_Call {
215+func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []string) *MockConfigService_FindAttachmentTargetsByImage_Call {
216216 _c.Call.Return(run)
217217 return _c
218218 }
219219
220-// FindAttachmentTargetsByImage provides a mock function for the type MockConfigService
221-func (_mock *MockConfigService) FindAttachmentTargetsByImage(ctx context.Context, imageName string) []string {
220+// FindRoutesByImage provides a mock function for the type MockConfigService
221+func (_mock *MockConfigService) FindRoutesByImage(ctx context.Context, imageName string) []domain.Route {
222222 ret := _mock.Called(ctx, imageName)
223223
224224 if len(ret) == 0 {
225- panic("no return value specified for FindAttachmentTargetsByImage")
225+ panic("no return value specified for FindRoutesByImage")
226226 }
227227
228- var r0 []string
229- if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok {
228+ var r0 []domain.Route
229+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Route); ok {
230230 r0 = returnFunc(ctx, imageName)
231231 } else {
232232 if ret.Get(0) != nil {
233- r0 = ret.Get(0).([]string)
233+ r0 = ret.Get(0).([]domain.Route)
234234 }
235235 }
236236 return r0
237237 }
238238
239-// MockConfigService_FindAttachmentTargetsByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindAttachmentTargetsByImage'
240-type MockConfigService_FindAttachmentTargetsByImage_Call struct {
239+// MockConfigService_FindRoutesByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRoutesByImage'
240+type MockConfigService_FindRoutesByImage_Call struct {
241241 *mock.Call
242242 }
243243
244-// FindAttachmentTargetsByImage is a helper method to define mock.On call
244+// FindRoutesByImage is a helper method to define mock.On call
245245 // - ctx context.Context
246246 // - imageName string
247-func (_e *MockConfigService_Expecter) FindAttachmentTargetsByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindAttachmentTargetsByImage_Call {
248- return &MockConfigService_FindAttachmentTargetsByImage_Call{Call: _e.mock.On("FindAttachmentTargetsByImage", ctx, imageName)}
247+func (_e *MockConfigService_Expecter) FindRoutesByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindRoutesByImage_Call {
248+ return &MockConfigService_FindRoutesByImage_Call{Call: _e.mock.On("FindRoutesByImage", ctx, imageName)}
249249 }
250250
251-func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindAttachmentTargetsByImage_Call {
251+func (_c *MockConfigService_FindRoutesByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindRoutesByImage_Call {
252252 _c.Call.Run(func(args mock.Arguments) {
253253 var arg0 context.Context
254254 if args[0] != nil {
@@ -266,12 +266,12 @@ func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Run(run func(ctx
266266 return _c
267267 }
268268
269-func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Return(targets []string) *MockConfigService_FindAttachmentTargetsByImage_Call {
270- _c.Call.Return(targets)
269+func (_c *MockConfigService_FindRoutesByImage_Call) Return(routes []domain.Route) *MockConfigService_FindRoutesByImage_Call {
270+ _c.Call.Return(routes)
271271 return _c
272272 }
273273
274-func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []string) *MockConfigService_FindAttachmentTargetsByImage_Call {
274+func (_c *MockConfigService_FindRoutesByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []domain.Route) *MockConfigService_FindRoutesByImage_Call {
275275 _c.Call.Return(run)
276276 return _c
277277 }
@@ -158,38 +158,38 @@ func (_c *MockConfigService_AddRoute_Call) RunAndReturn(run func(ctx context.Con
158 return _c158 return _c
159 }159 }
160 160
161-// FindRoutesByImage provides a mock function for the type MockConfigService161+// FindAttachmentTargetsByImage provides a mock function for the type MockConfigService
162-func (_mock *MockConfigService) FindRoutesByImage(ctx context.Context, imageName string) []domain.Route {162+func (_mock *MockConfigService) FindAttachmentTargetsByImage(ctx context.Context, imageName string) []string {
163 ret := _mock.Called(ctx, imageName)163 ret := _mock.Called(ctx, imageName)
164 164
165 if len(ret) == 0 {165 if len(ret) == 0 {
166- panic("no return value specified for FindRoutesByImage")166+ panic("no return value specified for FindAttachmentTargetsByImage")
167 }167 }
168 168
169- var r0 []domain.Route169+ var r0 []string
170- if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Route); ok {170+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok {
171 r0 = returnFunc(ctx, imageName)171 r0 = returnFunc(ctx, imageName)
172 } else {172 } else {
173 if ret.Get(0) != nil {173 if ret.Get(0) != nil {
174- r0 = ret.Get(0).([]domain.Route)174+ r0 = ret.Get(0).([]string)
175 }175 }
176 }176 }
177 return r0177 return r0
178 }178 }
179 179
180-// MockConfigService_FindRoutesByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRoutesByImage'180+// MockConfigService_FindAttachmentTargetsByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindAttachmentTargetsByImage'
181-type MockConfigService_FindRoutesByImage_Call struct {181+type MockConfigService_FindAttachmentTargetsByImage_Call struct {
182 *mock.Call182 *mock.Call
183 }183 }
184 184
185-// FindRoutesByImage is a helper method to define mock.On call185+// FindAttachmentTargetsByImage is a helper method to define mock.On call
186 // - ctx context.Context186 // - ctx context.Context
187 // - imageName string187 // - imageName string
188-func (_e *MockConfigService_Expecter) FindRoutesByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindRoutesByImage_Call {188+func (_e *MockConfigService_Expecter) FindAttachmentTargetsByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindAttachmentTargetsByImage_Call {
189- return &MockConfigService_FindRoutesByImage_Call{Call: _e.mock.On("FindRoutesByImage", ctx, imageName)}189+ return &MockConfigService_FindAttachmentTargetsByImage_Call{Call: _e.mock.On("FindAttachmentTargetsByImage", ctx, imageName)}
190 }190 }
191 191
192-func (_c *MockConfigService_FindRoutesByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindRoutesByImage_Call {192+func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindAttachmentTargetsByImage_Call {
193 _c.Call.Run(func(args mock.Arguments) {193 _c.Call.Run(func(args mock.Arguments) {
194 var arg0 context.Context194 var arg0 context.Context
195 if args[0] != nil {195 if args[0] != nil {
@@ -207,48 +207,48 @@ func (_c *MockConfigService_FindRoutesByImage_Call) Run(run func(ctx context.Con
207 return _c207 return _c
208 }208 }
209 209
210-func (_c *MockConfigService_FindRoutesByImage_Call) Return(routes []domain.Route) *MockConfigService_FindRoutesByImage_Call {210+func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Return(strings []string) *MockConfigService_FindAttachmentTargetsByImage_Call {
211- _c.Call.Return(routes)211+ _c.Call.Return(strings)
212 return _c212 return _c
213 }213 }
214 214
215-func (_c *MockConfigService_FindRoutesByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []domain.Route) *MockConfigService_FindRoutesByImage_Call {215+func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []string) *MockConfigService_FindAttachmentTargetsByImage_Call {
216 _c.Call.Return(run)216 _c.Call.Return(run)
217 return _c217 return _c
218 }218 }
219 219
220-// FindAttachmentTargetsByImage provides a mock function for the type MockConfigService220+// FindRoutesByImage provides a mock function for the type MockConfigService
221-func (_mock *MockConfigService) FindAttachmentTargetsByImage(ctx context.Context, imageName string) []string {221+func (_mock *MockConfigService) FindRoutesByImage(ctx context.Context, imageName string) []domain.Route {
222 ret := _mock.Called(ctx, imageName)222 ret := _mock.Called(ctx, imageName)
223 223
224 if len(ret) == 0 {224 if len(ret) == 0 {
225- panic("no return value specified for FindAttachmentTargetsByImage")225+ panic("no return value specified for FindRoutesByImage")
226 }226 }
227 227
228- var r0 []string228+ var r0 []domain.Route
229- if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok {229+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) []domain.Route); ok {
230 r0 = returnFunc(ctx, imageName)230 r0 = returnFunc(ctx, imageName)
231 } else {231 } else {
232 if ret.Get(0) != nil {232 if ret.Get(0) != nil {
233- r0 = ret.Get(0).([]string)233+ r0 = ret.Get(0).([]domain.Route)
234 }234 }
235 }235 }
236 return r0236 return r0
237 }237 }
238 238
239-// MockConfigService_FindAttachmentTargetsByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindAttachmentTargetsByImage'239+// MockConfigService_FindRoutesByImage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRoutesByImage'
240-type MockConfigService_FindAttachmentTargetsByImage_Call struct {240+type MockConfigService_FindRoutesByImage_Call struct {
241 *mock.Call241 *mock.Call
242 }242 }
243 243
244-// FindAttachmentTargetsByImage is a helper method to define mock.On call244+// FindRoutesByImage is a helper method to define mock.On call
245 // - ctx context.Context245 // - ctx context.Context
246 // - imageName string246 // - imageName string
247-func (_e *MockConfigService_Expecter) FindAttachmentTargetsByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindAttachmentTargetsByImage_Call {247+func (_e *MockConfigService_Expecter) FindRoutesByImage(ctx interface{}, imageName interface{}) *MockConfigService_FindRoutesByImage_Call {
248- return &MockConfigService_FindAttachmentTargetsByImage_Call{Call: _e.mock.On("FindAttachmentTargetsByImage", ctx, imageName)}248+ return &MockConfigService_FindRoutesByImage_Call{Call: _e.mock.On("FindRoutesByImage", ctx, imageName)}
249 }249 }
250 250
251-func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindAttachmentTargetsByImage_Call {251+func (_c *MockConfigService_FindRoutesByImage_Call) Run(run func(ctx context.Context, imageName string)) *MockConfigService_FindRoutesByImage_Call {
252 _c.Call.Run(func(args mock.Arguments) {252 _c.Call.Run(func(args mock.Arguments) {
253 var arg0 context.Context253 var arg0 context.Context
254 if args[0] != nil {254 if args[0] != nil {
@@ -266,12 +266,12 @@ func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Run(run func(ctx
266 return _c266 return _c
267 }267 }
268 268
269-func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) Return(targets []string) *MockConfigService_FindAttachmentTargetsByImage_Call {269+func (_c *MockConfigService_FindRoutesByImage_Call) Return(routes []domain.Route) *MockConfigService_FindRoutesByImage_Call {
270- _c.Call.Return(targets)270+ _c.Call.Return(routes)
271 return _c271 return _c
272 }272 }
273 273
274-func (_c *MockConfigService_FindAttachmentTargetsByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []string) *MockConfigService_FindAttachmentTargetsByImage_Call {274+func (_c *MockConfigService_FindRoutesByImage_Call) RunAndReturn(run func(ctx context.Context, imageName string) []domain.Route) *MockConfigService_FindRoutesByImage_Call {
275 _c.Call.Return(run)275 _c.Call.Return(run)
276 return _c276 return _c
277 }277 }
M pkg/registrypush/push.go +28 -1
@@ -4,6 +4,7 @@ import (
44 "bytes"
55 "context"
66 "crypto/sha256"
7+ "crypto/tls"
78 "encoding/base64"
89 "errors"
910 "fmt"
@@ -33,6 +34,7 @@ type ImageSource func(ctx context.Context, ref string) (v1.Image, error)
3334 type Pusher struct {
3435 chunkSize int64
3536 transport http.RoundTripper
37+ insecureTLS bool
3638 imageSource ImageSource
3739 progress io.Writer
3840 }
@@ -50,6 +52,12 @@ func WithTransport(t http.RoundTripper) Option {
5052 return func(p *Pusher) { p.transport = t }
5153 }
5254
55+// WithInsecureTLS disables TLS certificate verification for registry requests.
56+// This is ignored when a custom transport is provided via WithTransport.
57+func WithInsecureTLS(insecure bool) Option {
58+ return func(p *Pusher) { p.insecureTLS = insecure }
59+}
60+
5361 // WithImageSource overrides the default daemon-based image reader.
5462 func WithImageSource(src ImageSource) Option {
5563 return func(p *Pusher) { p.imageSource = src }
@@ -67,7 +75,11 @@ func New(opts ...Option) *Pusher {
6775 o(p)
6876 }
6977 if p.transport == nil {
70- p.transport = http.DefaultTransport
78+ if p.insecureTLS {
79+ p.transport = insecureTransport()
80+ } else {
81+ p.transport = http.DefaultTransport
82+ }
7183 }
7284 if p.imageSource == nil {
7385 p.imageSource = defaultImageSource
@@ -500,6 +512,21 @@ func chunkCount(size, chunkSize int64) int64 {
500512 return (size + chunkSize - 1) / chunkSize
501513 }
502514
515+func insecureTransport() http.RoundTripper {
516+ var transport *http.Transport
517+ if t, ok := http.DefaultTransport.(*http.Transport); ok {
518+ transport = t.Clone()
519+ } else {
520+ transport = &http.Transport{}
521+ }
522+ if transport.TLSClientConfig == nil {
523+ transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
524+ }
525+ //nolint:gosec // Explicit CLI opt-in via --insecure for self-signed/private cert deployments.
526+ transport.TLSClientConfig.InsecureSkipVerify = true
527+ return transport
528+}
529+
503530 func bytesReader(chunk []byte) io.Reader {
504531 return bytes.NewReader(chunk)
505532 }
@@ -4,6 +4,7 @@ import (
4 "bytes"4 "bytes"
5 "context"5 "context"
6 "crypto/sha256"6 "crypto/sha256"
7+ "crypto/tls"
7 "encoding/base64"8 "encoding/base64"
8 "errors"9 "errors"
9 "fmt"10 "fmt"
@@ -33,6 +34,7 @@ type ImageSource func(ctx context.Context, ref string) (v1.Image, error)
33 type Pusher struct {34 type Pusher struct {
34 chunkSize int6435 chunkSize int64
35 transport http.RoundTripper36 transport http.RoundTripper
37+ insecureTLS bool
36 imageSource ImageSource38 imageSource ImageSource
37 progress io.Writer39 progress io.Writer
38 }40 }
@@ -50,6 +52,12 @@ func WithTransport(t http.RoundTripper) Option {
50 return func(p *Pusher) { p.transport = t }52 return func(p *Pusher) { p.transport = t }
51 }53 }
52 54
55+// WithInsecureTLS disables TLS certificate verification for registry requests.
56+// This is ignored when a custom transport is provided via WithTransport.
57+func WithInsecureTLS(insecure bool) Option {
58+ return func(p *Pusher) { p.insecureTLS = insecure }
59+}
60+
53 // WithImageSource overrides the default daemon-based image reader.61 // WithImageSource overrides the default daemon-based image reader.
54 func WithImageSource(src ImageSource) Option {62 func WithImageSource(src ImageSource) Option {
55 return func(p *Pusher) { p.imageSource = src }63 return func(p *Pusher) { p.imageSource = src }
@@ -67,7 +75,11 @@ func New(opts ...Option) *Pusher {
67 o(p)75 o(p)
68 }76 }
69 if p.transport == nil {77 if p.transport == nil {
70- p.transport = http.DefaultTransport78+ if p.insecureTLS {
79+ p.transport = insecureTransport()
80+ } else {
81+ p.transport = http.DefaultTransport
82+ }
71 }83 }
72 if p.imageSource == nil {84 if p.imageSource == nil {
73 p.imageSource = defaultImageSource85 p.imageSource = defaultImageSource
@@ -500,6 +512,21 @@ func chunkCount(size, chunkSize int64) int64 {
500 return (size + chunkSize - 1) / chunkSize512 return (size + chunkSize - 1) / chunkSize
501 }513 }
502 514
515+func insecureTransport() http.RoundTripper {
516+ var transport *http.Transport
517+ if t, ok := http.DefaultTransport.(*http.Transport); ok {
518+ transport = t.Clone()
519+ } else {
520+ transport = &http.Transport{}
521+ }
522+ if transport.TLSClientConfig == nil {
523+ transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
524+ }
525+ //nolint:gosec // Explicit CLI opt-in via --insecure for self-signed/private cert deployments.
526+ transport.TLSClientConfig.InsecureSkipVerify = true
527+ return transport
528+}
529+
503 func bytesReader(chunk []byte) io.Reader {530 func bytesReader(chunk []byte) io.Reader {
504 return bytes.NewReader(chunk)531 return bytes.NewReader(chunk)
505 }532 }
M pkg/registrypush/push_test.go +39 -0
@@ -217,6 +217,45 @@ func TestPusher_Push(t *testing.T) {
217217 }
218218 }
219219
220+func TestPusher_WithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) {
221+ t.Parallel()
222+
223+ srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
224+ if r.Method == http.MethodHead {
225+ w.WriteHeader(http.StatusNotFound)
226+ return
227+ }
228+ if r.Method == http.MethodPost {
229+ w.Header().Set("Location", "/v2/repo/blobs/uploads/test-uuid")
230+ w.WriteHeader(http.StatusAccepted)
231+ return
232+ }
233+ if r.Method == http.MethodPatch {
234+ w.Header().Set("Location", "/v2/repo/blobs/uploads/test-uuid")
235+ w.WriteHeader(http.StatusAccepted)
236+ return
237+ }
238+ if r.Method == http.MethodPut {
239+ w.WriteHeader(http.StatusCreated)
240+ return
241+ }
242+ }))
243+ defer srv.Close()
244+
245+ ctx := context.Background()
246+ content := []byte("test-blob-data")
247+ digest := fmt.Sprintf("sha256:%x", sha256.Sum256(content))
248+
249+ pusherSecure := registrypush.New(registrypush.WithChunkSize(1024))
250+ err := pusherSecure.UploadBlob(ctx, srv.URL, "repo", digest, int64(len(content)), bytes.NewReader(content), "")
251+ require.Error(t, err)
252+ assert.Contains(t, err.Error(), "certificate")
253+
254+ pusherInsecure := registrypush.New(registrypush.WithChunkSize(1024), registrypush.WithInsecureTLS(true))
255+ err = pusherInsecure.UploadBlob(ctx, srv.URL, "repo", digest, int64(len(content)), bytes.NewReader(content), "")
256+ require.NoError(t, err)
257+}
258+
220259 type fakeBlobUploadRegistry struct {
221260 t *testing.T
222261 expectedAuth string
@@ -217,6 +217,45 @@ func TestPusher_Push(t *testing.T) {
217 }217 }
218 }218 }
219 219
220+func TestPusher_WithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) {
221+ t.Parallel()
222+
223+ srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
224+ if r.Method == http.MethodHead {
225+ w.WriteHeader(http.StatusNotFound)
226+ return
227+ }
228+ if r.Method == http.MethodPost {
229+ w.Header().Set("Location", "/v2/repo/blobs/uploads/test-uuid")
230+ w.WriteHeader(http.StatusAccepted)
231+ return
232+ }
233+ if r.Method == http.MethodPatch {
234+ w.Header().Set("Location", "/v2/repo/blobs/uploads/test-uuid")
235+ w.WriteHeader(http.StatusAccepted)
236+ return
237+ }
238+ if r.Method == http.MethodPut {
239+ w.WriteHeader(http.StatusCreated)
240+ return
241+ }
242+ }))
243+ defer srv.Close()
244+
245+ ctx := context.Background()
246+ content := []byte("test-blob-data")
247+ digest := fmt.Sprintf("sha256:%x", sha256.Sum256(content))
248+
249+ pusherSecure := registrypush.New(registrypush.WithChunkSize(1024))
250+ err := pusherSecure.UploadBlob(ctx, srv.URL, "repo", digest, int64(len(content)), bytes.NewReader(content), "")
251+ require.Error(t, err)
252+ assert.Contains(t, err.Error(), "certificate")
253+
254+ pusherInsecure := registrypush.New(registrypush.WithChunkSize(1024), registrypush.WithInsecureTLS(true))
255+ err = pusherInsecure.UploadBlob(ctx, srv.URL, "repo", digest, int64(len(content)), bytes.NewReader(content), "")
256+ require.NoError(t, err)
257+}
258+
220 type fakeBlobUploadRegistry struct {259 type fakeBlobUploadRegistry struct {
221 t *testing.T260 t *testing.T
222 expectedAuth string261 expectedAuth string

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close