Skip to content

Commit 1311546

Browse files
authored
cli: fix --build flag for create (docker#10982)
I missed this during a refactor and there wasn't test coverage. Instead of adding more heavy-weight integration tests, I tried to use `gomock` here to assert on the options objects after CLI flag parsing. I think with a few more helpers, this could be a good way to get a lot more combinations covered without adding a ton of slow E2E tests. Signed-off-by: Milas Bowman <[email protected]>
1 parent e1aa4f7 commit 1311546

File tree

3 files changed

+205
-16
lines changed

3 files changed

+205
-16
lines changed

cmd/compose/create.go

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ type createOptions struct {
4949

5050
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
5151
opts := createOptions{}
52+
buildOpts := buildOptions{
53+
ProjectOptions: p,
54+
}
5255
cmd := &cobra.Command{
5356
Use: "create [OPTIONS] [SERVICE...]",
5457
Short: "Creates containers for a service.",
@@ -62,20 +65,9 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
6265
}
6366
return nil
6467
}),
65-
RunE: p.WithProject(func(ctx context.Context, project *types.Project) error {
66-
if err := opts.Apply(project); err != nil {
67-
return err
68-
}
69-
return backend.Create(ctx, project, api.CreateOptions{
70-
RemoveOrphans: opts.removeOrphans,
71-
IgnoreOrphans: opts.ignoreOrphans,
72-
Recreate: opts.recreateStrategy(),
73-
RecreateDependencies: opts.dependenciesRecreateStrategy(),
74-
Inherit: !opts.noInherit,
75-
Timeout: opts.GetTimeout(),
76-
QuietPull: false,
77-
})
78-
}, dockerCli),
68+
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
69+
return runCreate(ctx, dockerCli, backend, opts, buildOpts, project, services)
70+
}),
7971
ValidArgsFunction: completeServiceNames(dockerCli, p),
8072
}
8173
flags := cmd.Flags()
@@ -89,6 +81,33 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
8981
return cmd
9082
}
9183

84+
func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
85+
if err := createOpts.Apply(project); err != nil {
86+
return err
87+
}
88+
89+
var build *api.BuildOptions
90+
if !createOpts.noBuild {
91+
bo, err := buildOpts.toAPIBuildOptions(services)
92+
if err != nil {
93+
return err
94+
}
95+
build = &bo
96+
}
97+
98+
return backend.Create(ctx, project, api.CreateOptions{
99+
Build: build,
100+
Services: services,
101+
RemoveOrphans: createOpts.removeOrphans,
102+
IgnoreOrphans: createOpts.ignoreOrphans,
103+
Recreate: createOpts.recreateStrategy(),
104+
RecreateDependencies: createOpts.dependenciesRecreateStrategy(),
105+
Inherit: !createOpts.noInherit,
106+
Timeout: createOpts.GetTimeout(),
107+
QuietPull: false,
108+
})
109+
}
110+
92111
func (opts createOptions) recreateStrategy() string {
93112
if opts.noRecreate {
94113
return api.RecreateNever

cmd/compose/create_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
Copyright 2023 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package compose
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"testing"
23+
24+
"github.com/compose-spec/compose-go/types"
25+
"github.com/davecgh/go-spew/spew"
26+
"github.com/docker/compose/v2/pkg/api"
27+
"github.com/docker/compose/v2/pkg/mocks"
28+
"github.com/golang/mock/gomock"
29+
"github.com/google/go-cmp/cmp"
30+
"github.com/stretchr/testify/require"
31+
)
32+
33+
func TestRunCreate(t *testing.T) {
34+
ctrl, ctx := gomock.WithContext(context.Background(), t)
35+
backend := mocks.NewMockService(ctrl)
36+
backend.EXPECT().Create(
37+
gomock.Eq(ctx),
38+
pullPolicy(""),
39+
deepEqual(defaultCreateOptions(true)),
40+
)
41+
42+
createOpts := createOptions{}
43+
buildOpts := buildOptions{}
44+
project := sampleProject()
45+
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
46+
require.NoError(t, err)
47+
}
48+
49+
func TestRunCreate_Build(t *testing.T) {
50+
ctrl, ctx := gomock.WithContext(context.Background(), t)
51+
backend := mocks.NewMockService(ctrl)
52+
backend.EXPECT().Create(
53+
gomock.Eq(ctx),
54+
pullPolicy("build"),
55+
deepEqual(defaultCreateOptions(true)),
56+
)
57+
58+
createOpts := createOptions{
59+
Build: true,
60+
}
61+
buildOpts := buildOptions{}
62+
project := sampleProject()
63+
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
64+
require.NoError(t, err)
65+
}
66+
67+
func TestRunCreate_NoBuild(t *testing.T) {
68+
ctrl, ctx := gomock.WithContext(context.Background(), t)
69+
backend := mocks.NewMockService(ctrl)
70+
backend.EXPECT().Create(
71+
gomock.Eq(ctx),
72+
pullPolicy(""),
73+
deepEqual(defaultCreateOptions(false)),
74+
)
75+
76+
createOpts := createOptions{
77+
noBuild: true,
78+
}
79+
buildOpts := buildOptions{}
80+
project := sampleProject()
81+
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
82+
require.NoError(t, err)
83+
}
84+
85+
func sampleProject() *types.Project {
86+
return &types.Project{
87+
Name: "test",
88+
Services: types.Services{
89+
{
90+
Name: "svc",
91+
Build: &types.BuildConfig{
92+
Context: ".",
93+
},
94+
},
95+
},
96+
}
97+
}
98+
99+
func defaultCreateOptions(includeBuild bool) api.CreateOptions {
100+
var build *api.BuildOptions
101+
if includeBuild {
102+
bo := defaultBuildOptions()
103+
build = &bo
104+
}
105+
return api.CreateOptions{
106+
Build: build,
107+
Services: nil,
108+
RemoveOrphans: false,
109+
IgnoreOrphans: false,
110+
Recreate: "diverged",
111+
RecreateDependencies: "diverged",
112+
Inherit: true,
113+
Timeout: nil,
114+
QuietPull: false,
115+
}
116+
}
117+
118+
func defaultBuildOptions() api.BuildOptions {
119+
return api.BuildOptions{
120+
Args: make(types.MappingWithEquals),
121+
Progress: "auto",
122+
}
123+
}
124+
125+
// deepEqual returns a nice diff on failure vs gomock.Eq when used
126+
// on structs.
127+
func deepEqual(x interface{}) gomock.Matcher {
128+
return gomock.GotFormatterAdapter(
129+
gomock.GotFormatterFunc(func(got interface{}) string {
130+
return cmp.Diff(x, got)
131+
}),
132+
gomock.Eq(x),
133+
)
134+
}
135+
136+
func spewAdapter(m gomock.Matcher) gomock.Matcher {
137+
return gomock.GotFormatterAdapter(
138+
gomock.GotFormatterFunc(func(got interface{}) string {
139+
return spew.Sdump(got)
140+
}),
141+
m,
142+
)
143+
}
144+
145+
type withPullPolicy struct {
146+
policy string
147+
}
148+
149+
func pullPolicy(policy string) gomock.Matcher {
150+
return spewAdapter(withPullPolicy{policy: policy})
151+
}
152+
153+
func (w withPullPolicy) Matches(x interface{}) bool {
154+
proj, ok := x.(*types.Project)
155+
if !ok || proj == nil || len(proj.Services) == 0 {
156+
return false
157+
}
158+
159+
for _, svc := range proj.Services {
160+
if svc.PullPolicy != w.policy {
161+
return false
162+
}
163+
}
164+
165+
return true
166+
}
167+
168+
func (w withPullPolicy) String() string {
169+
return fmt.Sprintf("has pull policy %q for all services", w.policy)
170+
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/containerd/console v1.0.3
1212
github.com/containerd/containerd v1.7.3
1313
github.com/cucumber/godog v0.0.0-00010101000000-000000000000 // replaced; see replace for the actual version used
14+
github.com/davecgh/go-spew v1.1.1
1415
github.com/distribution/reference v0.5.0
1516
github.com/docker/buildx v0.11.2
1617
github.com/docker/cli v24.0.5+incompatible
@@ -20,6 +21,7 @@ require (
2021
github.com/docker/go-units v0.5.0
2122
github.com/fsnotify/fsevents v0.1.1
2223
github.com/golang/mock v1.6.0
24+
github.com/google/go-cmp v0.5.9
2325
github.com/hashicorp/go-multierror v1.1.1
2426
github.com/hashicorp/go-version v1.6.0
2527
github.com/jonboulle/clockwork v0.4.0
@@ -75,7 +77,6 @@ require (
7577
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
7678
github.com/cucumber/messages-go/v16 v16.0.1 // indirect
7779
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
78-
github.com/davecgh/go-spew v1.1.1 // indirect
7980
github.com/docker/distribution v2.8.2+incompatible // indirect
8081
github.com/docker/docker-credential-helpers v0.7.0 // indirect
8182
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
@@ -94,7 +95,6 @@ require (
9495
github.com/gogo/protobuf v1.3.2 // indirect
9596
github.com/golang/protobuf v1.5.3 // indirect
9697
github.com/google/gnostic v0.5.7-v3refs // indirect
97-
github.com/google/go-cmp v0.5.9 // indirect
9898
github.com/google/gofuzz v1.2.0 // indirect
9999
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
100100
github.com/gorilla/mux v1.8.0 // indirect

0 commit comments

Comments
 (0)