detached 9a3a764

Merge pull request #108 from bnema/fix/107-native-push-tls

fix: honor TLS insecure settings in native registry push
commit
9a3a76484d5b3cb0f0fee8366c1859dafe42431c
author
bnema · · 2026-03-14 17:24 UTC
committer
GitHub · · 2026-03-14 17:24 UTC
merge of
d176775 + 63627a4 · diff against the first parent
signature
unsigned
10 files · +665 -146
M .mockery.yaml +8 -0
@@ -35,3 +35,11 @@ packages:
3535 HTTPProber:
3636 SecretService:
3737 LogService:
38+ # Exception: pushImageOps is a CLI-local interface, not a boundary port.
39+ # Mocked here because it abstracts Docker SDK calls that require a running
40+ # daemon, making unit/integration tests impractical without a test double.
41+ # Moving it to boundaries/out would violate interface segregation — no use
42+ # case depends on it.
43+ github.com/bnema/gordon/internal/adapters/in/cli:
44+ interfaces:
45+ pushImageOps:
@@ -35,3 +35,11 @@ packages:
35 HTTPProber:35 HTTPProber:
36 SecretService:36 SecretService:
37 LogService:37 LogService:
38+ # Exception: pushImageOps is a CLI-local interface, not a boundary port.
39+ # Mocked here because it abstracts Docker SDK calls that require a running
40+ # daemon, making unit/integration tests impractical without a test double.
41+ # Moving it to boundaries/out would violate interface segregation — no use
42+ # case depends on it.
43+ github.com/bnema/gordon/internal/adapters/in/cli:
44+ interfaces:
45+ pushImageOps:
M internal/adapters/in/cli/attachments_push.go +41 -15
@@ -15,11 +15,16 @@ import (
1515
1616 var (
1717 resolveControlPlaneFn = resolveControlPlane
18- tagAndPushFn = tagAndPush
19- buildAndPushFn = buildAndPush
2018 determineVersionFn = determineVersion
2119 )
2220
21+// attachmentPushRequest holds all inputs for the attachments push command.
22+type attachmentPushRequest struct {
23+ ImageArg string
24+ Tag string
25+ Build buildConfig
26+}
27+
2328 func newAttachmentsPushCmd() *cobra.Command {
2429 var (
2530 build bool
@@ -34,7 +39,11 @@ func newAttachmentsPushCmd() *cobra.Command {
3439 Short: "Build, tag, and push an attachment image",
3540 Args: cobra.ExactArgs(1),
3641 RunE: func(cmd *cobra.Command, args []string) error {
37- return runAttachmentsPush(cmd.Context(), args[0], tag, build, platform, dockerfile, buildArgs, cmd.OutOrStdout())
42+ return runAttachmentsPush(cmd.Context(), attachmentPushRequest{
43+ ImageArg: args[0],
44+ Tag: tag,
45+ Build: buildConfig{Enabled: build, Platform: platform, Dockerfile: dockerfile, BuildArgs: buildArgs},
46+ }, cmd.OutOrStdout())
3847 },
3948 }
4049
@@ -47,41 +56,58 @@ func newAttachmentsPushCmd() *cobra.Command {
4756 return cmd
4857 }
4958
50-func runAttachmentsPush(ctx context.Context, imageArg, tag string, build bool, platform string, dockerfile string, buildArgs []string, out io.Writer) error {
59+func runAttachmentsPush(ctx context.Context, req attachmentPushRequest, out io.Writer) error {
5160 handle, err := resolveControlPlaneFn(configPath)
5261 if err != nil {
5362 return err
5463 }
5564 defer handle.close()
5665
57- dockerfile, err = resolveDockerfile(dockerfile, build)
66+ dockerfile, err := resolveDockerfile(req.Build.Dockerfile, req.Build.Enabled)
5867 if err != nil {
5968 return err
6069 }
6170
62- for _, ba := range buildArgs {
71+ for _, ba := range req.Build.BuildArgs {
6372 if err := validateBuildArg(ba); err != nil {
6473 return err
6574 }
6675 }
6776
68- registry, imageName, targets, err := resolveAttachmentImage(ctx, handle.plane, imageArg)
77+ registry, imageName, targets, err := resolveAttachmentImage(ctx, handle.plane, req.ImageArg)
6978 if err != nil {
7079 return err
7180 }
7281
73- version, err := resolveVersionWithFn(ctx, tag)
82+ version, err := resolveVersionWithFn(ctx, req.Tag)
7483 if err != nil {
7584 return err
7685 }
7786
78- versionRef, latestRef := resolveImageRefs(registry, imageName, version)
87+ img := imagePush{
88+ Registry: registry,
89+ ImageName: imageName,
90+ Version: version,
91+ }
92+ img.VersionRef, img.LatestRef = resolveImageRefs(registry, imageName, version)
7993
80- if err := printAttachmentPushInfo(out, imageArg, targets, versionRef, latestRef, version); err != nil {
94+ imageOps, err := newImageOpsFn()
95+ if err != nil {
8196 return err
8297 }
8398
84- if err := performAttachmentPush(ctx, build, version, platform, dockerfile, buildArgs, registry, imageName, versionRef, latestRef); err != nil {
99+ if err := printAttachmentPushInfo(out, req.ImageArg, targets, img.VersionRef, img.LatestRef, version); err != nil {
100+ return err
101+ }
102+
103+ build := buildConfig{
104+ Enabled: req.Build.Enabled,
105+ Platform: req.Build.Platform,
106+ Dockerfile: dockerfile,
107+ BuildArgs: req.Build.BuildArgs,
108+ }
109+
110+ if err := performAttachmentPush(ctx, imageOps, build, img); err != nil {
85111 return err
86112 }
87113
@@ -106,11 +132,11 @@ func printAttachmentPushInfo(out io.Writer, imageArg string, targets []string, v
106132 return nil
107133 }
108134
109-func performAttachmentPush(ctx context.Context, build bool, version, platform, dockerfile string, buildArgs []string, registry, imageName, versionRef, latestRef string) error {
110- if build {
111- return buildAndPushFn(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)
135+func performAttachmentPush(ctx context.Context, ops pushImageOps, build buildConfig, img imagePush) error {
136+ if build.Enabled {
137+ return buildAndPush(ctx, ops, build, img)
112138 }
113- return tagAndPushFn(ctx, registry, imageName, version, versionRef, latestRef)
139+ return tagAndPush(ctx, ops, img)
114140 }
115141
116142 func resolveAttachmentImage(ctx context.Context, cp ControlPlane, imageArg string) (registry, imageName string, targets []string, err error) {
@@ -15,11 +15,16 @@ 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
21+// attachmentPushRequest holds all inputs for the attachments push command.
22+type attachmentPushRequest struct {
23+ ImageArg string
24+ Tag string
25+ Build buildConfig
26+}
27+
23 func newAttachmentsPushCmd() *cobra.Command {28 func newAttachmentsPushCmd() *cobra.Command {
24 var (29 var (
25 build bool30 build bool
@@ -34,7 +39,11 @@ func newAttachmentsPushCmd() *cobra.Command {
34 Short: "Build, tag, and push an attachment image",39 Short: "Build, tag, and push an attachment image",
35 Args: cobra.ExactArgs(1),40 Args: cobra.ExactArgs(1),
36 RunE: func(cmd *cobra.Command, args []string) error {41 RunE: func(cmd *cobra.Command, args []string) error {
37- return runAttachmentsPush(cmd.Context(), args[0], tag, build, platform, dockerfile, buildArgs, cmd.OutOrStdout())42+ return runAttachmentsPush(cmd.Context(), attachmentPushRequest{
43+ ImageArg: args[0],
44+ Tag: tag,
45+ Build: buildConfig{Enabled: build, Platform: platform, Dockerfile: dockerfile, BuildArgs: buildArgs},
46+ }, cmd.OutOrStdout())
38 },47 },
39 }48 }
40 49
@@ -47,41 +56,58 @@ func newAttachmentsPushCmd() *cobra.Command {
47 return cmd56 return cmd
48 }57 }
49 58
50-func runAttachmentsPush(ctx context.Context, imageArg, tag string, build bool, platform string, dockerfile string, buildArgs []string, out io.Writer) error {59+func runAttachmentsPush(ctx context.Context, req attachmentPushRequest, out io.Writer) error {
51 handle, err := resolveControlPlaneFn(configPath)60 handle, err := resolveControlPlaneFn(configPath)
52 if err != nil {61 if err != nil {
53 return err62 return err
54 }63 }
55 defer handle.close()64 defer handle.close()
56 65
57- dockerfile, err = resolveDockerfile(dockerfile, build)66+ dockerfile, err := resolveDockerfile(req.Build.Dockerfile, req.Build.Enabled)
58 if err != nil {67 if err != nil {
59 return err68 return err
60 }69 }
61 70
62- for _, ba := range buildArgs {71+ for _, ba := range req.Build.BuildArgs {
63 if err := validateBuildArg(ba); err != nil {72 if err := validateBuildArg(ba); err != nil {
64 return err73 return err
65 }74 }
66 }75 }
67 76
68- registry, imageName, targets, err := resolveAttachmentImage(ctx, handle.plane, imageArg)77+ registry, imageName, targets, err := resolveAttachmentImage(ctx, handle.plane, req.ImageArg)
69 if err != nil {78 if err != nil {
70 return err79 return err
71 }80 }
72 81
73- version, err := resolveVersionWithFn(ctx, tag)82+ version, err := resolveVersionWithFn(ctx, req.Tag)
74 if err != nil {83 if err != nil {
75 return err84 return err
76 }85 }
77 86
78- versionRef, latestRef := resolveImageRefs(registry, imageName, version)87+ img := imagePush{
88+ Registry: registry,
89+ ImageName: imageName,
90+ Version: version,
91+ }
92+ img.VersionRef, img.LatestRef = resolveImageRefs(registry, imageName, version)
79 93
80- if err := printAttachmentPushInfo(out, imageArg, targets, versionRef, latestRef, version); err != nil {94+ imageOps, err := newImageOpsFn()
95+ if err != nil {
81 return err96 return err
82 }97 }
83 98
84- if err := performAttachmentPush(ctx, build, version, platform, dockerfile, buildArgs, registry, imageName, versionRef, latestRef); err != nil {99+ if err := printAttachmentPushInfo(out, req.ImageArg, targets, img.VersionRef, img.LatestRef, version); err != nil {
100+ return err
101+ }
102+
103+ build := buildConfig{
104+ Enabled: req.Build.Enabled,
105+ Platform: req.Build.Platform,
106+ Dockerfile: dockerfile,
107+ BuildArgs: req.Build.BuildArgs,
108+ }
109+
110+ if err := performAttachmentPush(ctx, imageOps, build, img); err != nil {
85 return err111 return err
86 }112 }
87 113
@@ -106,11 +132,11 @@ func printAttachmentPushInfo(out io.Writer, imageArg string, targets []string, v
106 return nil132 return nil
107 }133 }
108 134
109-func performAttachmentPush(ctx context.Context, build bool, version, platform, dockerfile string, buildArgs []string, registry, imageName, versionRef, latestRef string) error {135+func performAttachmentPush(ctx context.Context, ops pushImageOps, build buildConfig, img imagePush) error {
110- if build {136+ if build.Enabled {
111- return buildAndPushFn(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)137+ return buildAndPush(ctx, ops, build, img)
112 }138 }
113- return tagAndPushFn(ctx, registry, imageName, version, versionRef, latestRef)139+ return tagAndPush(ctx, ops, img)
114 }140 }
115 141
116 func resolveAttachmentImage(ctx context.Context, cp ControlPlane, imageArg string) (registry, imageName string, targets []string, err error) {142 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 +94 -69
@@ -18,15 +18,37 @@ 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
25+
26+// buildConfig holds image build settings for push commands.
27+type buildConfig struct {
28+ Enabled bool
29+ Platform string
30+ Dockerfile string
31+ BuildArgs []string
32+}
33+
34+// imagePush holds resolved image identity for push operations.
35+type imagePush struct {
36+ Registry string
37+ ImageName string
38+ Version string
39+ VersionRef string
40+ LatestRef string
41+}
42+
43+// pushRequest holds all inputs for the push command.
44+type pushRequest struct {
45+ ImageArg string
46+ Domain string
47+ Tag string
48+ Build buildConfig
49+ NoDeploy bool
50+ NoConfirm bool
51+}
3052
3153 func newPushCmd() *cobra.Command {
3254 var (
@@ -65,7 +87,14 @@ Examples:
6587 if len(args) > 0 {
6688 imageArg = args[0]
6789 }
68- return runPush(cmd.Context(), imageArg, domainFlag, tag, build, platform, dockerfile, buildArgs, noDeploy, noConfirm)
90+ return runPush(cmd.Context(), pushRequest{
91+ ImageArg: imageArg,
92+ Domain: domainFlag,
93+ Tag: tag,
94+ Build: buildConfig{Enabled: build, Platform: platform, Dockerfile: dockerfile, BuildArgs: buildArgs},
95+ NoDeploy: noDeploy,
96+ NoConfirm: noConfirm,
97+ })
6998 },
7099 }
71100
@@ -125,65 +154,82 @@ func resolveVersion(ctx context.Context, tag string) (string, error) {
125154 return version, nil
126155 }
127156
128-func runPush(ctx context.Context, imageArg, domainFlag, tag string, build bool, platform string, dockerfile string, buildArgs []string, noDeploy bool, noConfirm bool) error {
157+func runPush(ctx context.Context, req pushRequest) error {
129158 handle, err := resolveControlPlane(configPath)
130159 if err != nil {
131160 return err
132161 }
133162 defer handle.close()
134163
135- dockerfile, err = resolveDockerfile(dockerfile, build)
164+ dockerfile, err := resolveDockerfile(req.Build.Dockerfile, req.Build.Enabled)
136165 if err != nil {
137166 return err
138167 }
139168
140- registry, imageName, pushDomain, err := resolveRoute(ctx, handle.plane, imageArg, domainFlag, dockerfile)
169+ registry, imageName, pushDomain, err := resolveRoute(ctx, handle.plane, req.ImageArg, req.Domain, dockerfile)
141170 if err != nil {
142171 return err
143172 }
144173
145- version, err := resolveVersion(ctx, tag)
174+ version, err := resolveVersion(ctx, req.Tag)
146175 if err != nil {
147176 return err
148177 }
149178
150- for _, ba := range buildArgs {
179+ for _, ba := range req.Build.BuildArgs {
151180 if err := validateBuildArg(ba); err != nil {
152181 return err
153182 }
154183 }
155184
156- versionRef, latestRef := resolveImageRefs(registry, imageName, version)
185+ img := imagePush{
186+ Registry: registry,
187+ ImageName: imageName,
188+ Version: version,
189+ }
190+ img.VersionRef, img.LatestRef = resolveImageRefs(registry, imageName, version)
157191
158- fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(versionRef))
192+ imageOps, err := newImageOpsFn()
193+ if err != nil {
194+ return err
195+ }
196+
197+ fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(img.VersionRef))
159198 if version != "latest" {
160- fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(latestRef))
199+ fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(img.LatestRef))
161200 }
162201 fmt.Printf("Domain: %s\n", styles.Theme.Bold.Render(pushDomain))
163202
164203 // Signal the server to suppress event-based deploys for this image.
165204 // The CLI will trigger an explicit deploy after push completes.
166- if !noDeploy {
205+ if !req.NoDeploy {
167206 if err := handle.plane.DeployIntent(ctx, imageName); err != nil {
168207 // Non-fatal: worst case we get a redundant deploy via event
169208 fmt.Fprintf(os.Stderr, "warning: failed to register deploy intent: %v\n", err)
170209 }
171210 }
172211
173- if build {
174- if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {
212+ build := buildConfig{
213+ Enabled: req.Build.Enabled,
214+ Platform: req.Build.Platform,
215+ Dockerfile: dockerfile,
216+ BuildArgs: req.Build.BuildArgs,
217+ }
218+
219+ if build.Enabled {
220+ if err := buildAndPush(ctx, imageOps, build, img); err != nil {
175221 return err
176222 }
177223 } else {
178- if err := tagAndPush(ctx, registry, imageName, version, versionRef, latestRef); err != nil {
224+ if err := tagAndPush(ctx, imageOps, img); err != nil {
179225 return err
180226 }
181227 }
182228
183229 fmt.Println(styles.RenderSuccess("Push complete"))
184230
185- if !noDeploy {
186- return deployAfterPush(ctx, handle.plane, pushDomain, noConfirm)
231+ if !req.NoDeploy {
232+ return deployAfterPush(ctx, handle.plane, pushDomain, req.NoConfirm)
187233 }
188234
189235 return nil
@@ -474,30 +520,26 @@ func parseTagRef(ref string) string {
474520 return tag
475521 }
476522
477-func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {
478- if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
479- return fmt.Errorf("dockerfile not found: %s", dockerfile)
523+func buildAndPush(ctx context.Context, ops pushImageOps, build buildConfig, img imagePush) error {
524+ if _, err := os.Stat(build.Dockerfile); os.IsNotExist(err) {
525+ return fmt.Errorf("dockerfile not found: %s", build.Dockerfile)
480526 }
481527
482528 // Build and load into local daemon (NOT --push).
483529 // The native registry push client handles chunked uploads to stay
484530 // within Cloudflare's 100MB per-request limit.
485531 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)
532+ if err := ops.Build(ctx, buildImageArgs(ctx, img.Version, build.Platform, build.Dockerfile, build.BuildArgs, img.VersionRef, img.LatestRef)); err != nil {
533+ return err
492534 }
493535
494536 fmt.Println("Pushing...")
495- if err := dockerPushFn(ctx, latestRef); err != nil {
496- return fmt.Errorf("failed to push %s: %w", latestRef, err)
537+ if err := ops.Push(ctx, img.LatestRef); err != nil {
538+ return fmt.Errorf("failed to push %s: %w", img.LatestRef, err)
497539 }
498- if version != "latest" {
499- if err := dockerPushFn(ctx, versionRef); err != nil {
500- return fmt.Errorf("failed to push %s: %w", versionRef, err)
540+ if img.Version != "latest" {
541+ if err := ops.Push(ctx, img.VersionRef); err != nil {
542+ return fmt.Errorf("failed to push %s: %w", img.VersionRef, err)
501543 }
502544 }
503545
@@ -554,31 +596,35 @@ func buildImageArgs(ctx context.Context, version, platform, dockerfile string, b
554596 return args
555597 }
556598
557-func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error {
558- localImage := fmt.Sprintf("%s/%s", registry, imageName)
599+func tagAndPush(ctx context.Context, ops pushImageOps, img imagePush) error {
600+ localImage := fmt.Sprintf("%s/%s", img.Registry, img.ImageName)
559601
560602 fmt.Println("\nChecking local image...")
561- if !dockerImageExistsFn(ctx, localImage) {
603+ exists, err := ops.Exists(ctx, localImage)
604+ if err != nil {
605+ return fmt.Errorf("failed to inspect local image %s: %w", localImage, err)
606+ }
607+ if !exists {
562608 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)
563609 }
564610
565611 fmt.Println("Tagging...")
566- if err := dockerTagFn(ctx, localImage, versionRef); err != nil {
567- return fmt.Errorf("failed to tag %s: %w", versionRef, err)
612+ if err := ops.Tag(ctx, localImage, img.VersionRef); err != nil {
613+ return fmt.Errorf("failed to tag %s: %w", img.VersionRef, err)
568614 }
569- if version != "latest" {
570- if err := dockerTagFn(ctx, localImage, latestRef); err != nil {
571- return fmt.Errorf("failed to tag %s: %w", latestRef, err)
615+ if img.Version != "latest" {
616+ if err := ops.Tag(ctx, localImage, img.LatestRef); err != nil {
617+ return fmt.Errorf("failed to tag %s: %w", img.LatestRef, err)
572618 }
573619 }
574620
575621 fmt.Println("Pushing...")
576- if err := dockerPushFn(ctx, versionRef); err != nil {
577- return fmt.Errorf("failed to push %s: %w", versionRef, err)
622+ if err := ops.Push(ctx, img.VersionRef); err != nil {
623+ return fmt.Errorf("failed to push %s: %w", img.VersionRef, err)
578624 }
579- if version != "latest" {
580- if err := dockerPushFn(ctx, latestRef); err != nil {
581- return fmt.Errorf("failed to push %s: %w", latestRef, err)
625+ if img.Version != "latest" {
626+ if err := ops.Push(ctx, img.LatestRef); err != nil {
627+ return fmt.Errorf("failed to push %s: %w", img.LatestRef, err)
582628 }
583629 }
584630 return nil
@@ -742,27 +788,6 @@ func getGitVersion(ctx context.Context) string {
742788 return strings.TrimSpace(string(out))
743789 }
744790
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-
766791 // buildArgPattern matches KEY=VALUE where KEY starts with a letter or underscore.
767792 var buildArgPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*$`)
768793
@@ -18,15 +18,37 @@ 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 = dockerTag25+
27- dockerPushFn = dockerPush26+// buildConfig holds image build settings for push commands.
28- dockerImageExistsFn = dockerImageExists27+type buildConfig struct {
29-)28+ Enabled bool
29+ Platform string
30+ Dockerfile string
31+ BuildArgs []string
32+}
33+
34+// imagePush holds resolved image identity for push operations.
35+type imagePush struct {
36+ Registry string
37+ ImageName string
38+ Version string
39+ VersionRef string
40+ LatestRef string
41+}
42+
43+// pushRequest holds all inputs for the push command.
44+type pushRequest struct {
45+ ImageArg string
46+ Domain string
47+ Tag string
48+ Build buildConfig
49+ NoDeploy bool
50+ NoConfirm bool
51+}
30 52
31 func newPushCmd() *cobra.Command {53 func newPushCmd() *cobra.Command {
32 var (54 var (
@@ -65,7 +87,14 @@ Examples:
65 if len(args) > 0 {87 if len(args) > 0 {
66 imageArg = args[0]88 imageArg = args[0]
67 }89 }
68- return runPush(cmd.Context(), imageArg, domainFlag, tag, build, platform, dockerfile, buildArgs, noDeploy, noConfirm)90+ return runPush(cmd.Context(), pushRequest{
91+ ImageArg: imageArg,
92+ Domain: domainFlag,
93+ Tag: tag,
94+ Build: buildConfig{Enabled: build, Platform: platform, Dockerfile: dockerfile, BuildArgs: buildArgs},
95+ NoDeploy: noDeploy,
96+ NoConfirm: noConfirm,
97+ })
69 },98 },
70 }99 }
71 100
@@ -125,65 +154,82 @@ func resolveVersion(ctx context.Context, tag string) (string, error) {
125 return version, nil154 return version, nil
126 }155 }
127 156
128-func runPush(ctx context.Context, imageArg, domainFlag, tag string, build bool, platform string, dockerfile string, buildArgs []string, noDeploy bool, noConfirm bool) error {157+func runPush(ctx context.Context, req pushRequest) error {
129 handle, err := resolveControlPlane(configPath)158 handle, err := resolveControlPlane(configPath)
130 if err != nil {159 if err != nil {
131 return err160 return err
132 }161 }
133 defer handle.close()162 defer handle.close()
134 163
135- dockerfile, err = resolveDockerfile(dockerfile, build)164+ dockerfile, err := resolveDockerfile(req.Build.Dockerfile, req.Build.Enabled)
136 if err != nil {165 if err != nil {
137 return err166 return err
138 }167 }
139 168
140- registry, imageName, pushDomain, err := resolveRoute(ctx, handle.plane, imageArg, domainFlag, dockerfile)169+ registry, imageName, pushDomain, err := resolveRoute(ctx, handle.plane, req.ImageArg, req.Domain, dockerfile)
141 if err != nil {170 if err != nil {
142 return err171 return err
143 }172 }
144 173
145- version, err := resolveVersion(ctx, tag)174+ version, err := resolveVersion(ctx, req.Tag)
146 if err != nil {175 if err != nil {
147 return err176 return err
148 }177 }
149 178
150- for _, ba := range buildArgs {179+ for _, ba := range req.Build.BuildArgs {
151 if err := validateBuildArg(ba); err != nil {180 if err := validateBuildArg(ba); err != nil {
152 return err181 return err
153 }182 }
154 }183 }
155 184
156- versionRef, latestRef := resolveImageRefs(registry, imageName, version)185+ img := imagePush{
186+ Registry: registry,
187+ ImageName: imageName,
188+ Version: version,
189+ }
190+ img.VersionRef, img.LatestRef = resolveImageRefs(registry, imageName, version)
157 191
158- fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(versionRef))192+ imageOps, err := newImageOpsFn()
193+ if err != nil {
194+ return err
195+ }
196+
197+ fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(img.VersionRef))
159 if version != "latest" {198 if version != "latest" {
160- fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(latestRef))199+ fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(img.LatestRef))
161 }200 }
162 fmt.Printf("Domain: %s\n", styles.Theme.Bold.Render(pushDomain))201 fmt.Printf("Domain: %s\n", styles.Theme.Bold.Render(pushDomain))
163 202
164 // Signal the server to suppress event-based deploys for this image.203 // Signal the server to suppress event-based deploys for this image.
165 // The CLI will trigger an explicit deploy after push completes.204 // The CLI will trigger an explicit deploy after push completes.
166- if !noDeploy {205+ if !req.NoDeploy {
167 if err := handle.plane.DeployIntent(ctx, imageName); err != nil {206 if err := handle.plane.DeployIntent(ctx, imageName); err != nil {
168 // Non-fatal: worst case we get a redundant deploy via event207 // Non-fatal: worst case we get a redundant deploy via event
169 fmt.Fprintf(os.Stderr, "warning: failed to register deploy intent: %v\n", err)208 fmt.Fprintf(os.Stderr, "warning: failed to register deploy intent: %v\n", err)
170 }209 }
171 }210 }
172 211
173- if build {212+ build := buildConfig{
174- if err := buildAndPush(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef); err != nil {213+ Enabled: req.Build.Enabled,
214+ Platform: req.Build.Platform,
215+ Dockerfile: dockerfile,
216+ BuildArgs: req.Build.BuildArgs,
217+ }
218+
219+ if build.Enabled {
220+ if err := buildAndPush(ctx, imageOps, build, img); err != nil {
175 return err221 return err
176 }222 }
177 } else {223 } else {
178- if err := tagAndPush(ctx, registry, imageName, version, versionRef, latestRef); err != nil {224+ if err := tagAndPush(ctx, imageOps, img); err != nil {
179 return err225 return err
180 }226 }
181 }227 }
182 228
183 fmt.Println(styles.RenderSuccess("Push complete"))229 fmt.Println(styles.RenderSuccess("Push complete"))
184 230
185- if !noDeploy {231+ if !req.NoDeploy {
186- return deployAfterPush(ctx, handle.plane, pushDomain, noConfirm)232+ return deployAfterPush(ctx, handle.plane, pushDomain, req.NoConfirm)
187 }233 }
188 234
189 return nil235 return nil
@@ -474,30 +520,26 @@ func parseTagRef(ref string) string {
474 return tag520 return tag
475 }521 }
476 522
477-func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {523+func buildAndPush(ctx context.Context, ops pushImageOps, build buildConfig, img imagePush) error {
478- if _, err := os.Stat(dockerfile); os.IsNotExist(err) {524+ if _, err := os.Stat(build.Dockerfile); os.IsNotExist(err) {
479- return fmt.Errorf("dockerfile not found: %s", dockerfile)525+ return fmt.Errorf("dockerfile not found: %s", build.Dockerfile)
480 }526 }
481 527
482 // Build and load into local daemon (NOT --push).528 // Build and load into local daemon (NOT --push).
483 // The native registry push client handles chunked uploads to stay529 // The native registry push client handles chunked uploads to stay
484 // within Cloudflare's 100MB per-request limit.530 // within Cloudflare's 100MB per-request limit.
485 fmt.Println("\nBuilding image...")531 fmt.Println("\nBuilding image...")
486- buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(ctx, version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204532+ if err := ops.Build(ctx, buildImageArgs(ctx, img.Version, build.Platform, build.Dockerfile, build.BuildArgs, img.VersionRef, img.LatestRef)); err != nil {
487- buildCmd.Env = os.Environ() // VERSION is now passed as --build-arg VERSION=<value>533+ 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 }534 }
493 535
494 fmt.Println("Pushing...")536 fmt.Println("Pushing...")
495- if err := dockerPushFn(ctx, latestRef); err != nil {537+ if err := ops.Push(ctx, img.LatestRef); err != nil {
496- return fmt.Errorf("failed to push %s: %w", latestRef, err)538+ return fmt.Errorf("failed to push %s: %w", img.LatestRef, err)
497 }539 }
498- if version != "latest" {540+ if img.Version != "latest" {
499- if err := dockerPushFn(ctx, versionRef); err != nil {541+ if err := ops.Push(ctx, img.VersionRef); err != nil {
500- return fmt.Errorf("failed to push %s: %w", versionRef, err)542+ return fmt.Errorf("failed to push %s: %w", img.VersionRef, err)
501 }543 }
502 }544 }
503 545
@@ -554,31 +596,35 @@ func buildImageArgs(ctx context.Context, version, platform, dockerfile string, b
554 return args596 return args
555 }597 }
556 598
557-func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error {599+func tagAndPush(ctx context.Context, ops pushImageOps, img imagePush) error {
558- localImage := fmt.Sprintf("%s/%s", registry, imageName)600+ localImage := fmt.Sprintf("%s/%s", img.Registry, img.ImageName)
559 601
560 fmt.Println("\nChecking local image...")602 fmt.Println("\nChecking local image...")
561- if !dockerImageExistsFn(ctx, localImage) {603+ exists, err := ops.Exists(ctx, localImage)
604+ if err != nil {
605+ return fmt.Errorf("failed to inspect local image %s: %w", localImage, err)
606+ }
607+ if !exists {
562 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)608 return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage)
563 }609 }
564 610
565 fmt.Println("Tagging...")611 fmt.Println("Tagging...")
566- if err := dockerTagFn(ctx, localImage, versionRef); err != nil {612+ if err := ops.Tag(ctx, localImage, img.VersionRef); err != nil {
567- return fmt.Errorf("failed to tag %s: %w", versionRef, err)613+ return fmt.Errorf("failed to tag %s: %w", img.VersionRef, err)
568 }614 }
569- if version != "latest" {615+ if img.Version != "latest" {
570- if err := dockerTagFn(ctx, localImage, latestRef); err != nil {616+ if err := ops.Tag(ctx, localImage, img.LatestRef); err != nil {
571- return fmt.Errorf("failed to tag %s: %w", latestRef, err)617+ return fmt.Errorf("failed to tag %s: %w", img.LatestRef, err)
572 }618 }
573 }619 }
574 620
575 fmt.Println("Pushing...")621 fmt.Println("Pushing...")
576- if err := dockerPushFn(ctx, versionRef); err != nil {622+ if err := ops.Push(ctx, img.VersionRef); err != nil {
577- return fmt.Errorf("failed to push %s: %w", versionRef, err)623+ return fmt.Errorf("failed to push %s: %w", img.VersionRef, err)
578 }624 }
579- if version != "latest" {625+ if img.Version != "latest" {
580- if err := dockerPushFn(ctx, latestRef); err != nil {626+ if err := ops.Push(ctx, img.LatestRef); err != nil {
581- return fmt.Errorf("failed to push %s: %w", latestRef, err)627+ return fmt.Errorf("failed to push %s: %w", img.LatestRef, err)
582 }628 }
583 }629 }
584 return nil630 return nil
@@ -742,27 +788,6 @@ func getGitVersion(ctx context.Context) string {
742 return strings.TrimSpace(string(out))788 return strings.TrimSpace(string(out))
743 }789 }
744 790
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.791 // buildArgPattern matches KEY=VALUE where KEY starts with a letter or underscore.
767 var buildArgPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*$`)792 var buildArgPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*$`)
768 793
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 +14 -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,19 @@ 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, imagePush{
55+ Registry: registry,
56+ ImageName: "testapp",
57+ Version: "v1.0.0",
58+ VersionRef: versionRef,
59+ LatestRef: latestRef,
60+ })
6561 require.NoError(t, err)
6662
6763 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,19 @@ 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, imagePush{
55+ Registry: registry,
56+ ImageName: "testapp",
57+ Version: "v1.0.0",
58+ VersionRef: versionRef,
59+ LatestRef: latestRef,
60+ })
65 require.NoError(t, err)61 require.NoError(t, err)
66 62
67 harness.RequireManifest(t, "v1.0.0")63 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