Skip to content

Commit

Permalink
cli: fix --build flag for create (#10982)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
milas authored Sep 8, 2023
1 parent e1aa4f7 commit 1311546
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 16 deletions.
47 changes: 33 additions & 14 deletions cmd/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type createOptions struct {

func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "create [OPTIONS] [SERVICE...]",
Short: "Creates containers for a service.",
Expand All @@ -62,20 +65,9 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
}
return nil
}),
RunE: p.WithProject(func(ctx context.Context, project *types.Project) error {
if err := opts.Apply(project); err != nil {
return err
}
return backend.Create(ctx, project, api.CreateOptions{
RemoveOrphans: opts.removeOrphans,
IgnoreOrphans: opts.ignoreOrphans,
Recreate: opts.recreateStrategy(),
RecreateDependencies: opts.dependenciesRecreateStrategy(),
Inherit: !opts.noInherit,
Timeout: opts.GetTimeout(),
QuietPull: false,
})
}, dockerCli),
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
return runCreate(ctx, dockerCli, backend, opts, buildOpts, project, services)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
Expand All @@ -89,6 +81,33 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return cmd
}

func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
if err := createOpts.Apply(project); err != nil {
return err
}

var build *api.BuildOptions
if !createOpts.noBuild {
bo, err := buildOpts.toAPIBuildOptions(services)
if err != nil {
return err
}
build = &bo
}

return backend.Create(ctx, project, api.CreateOptions{
Build: build,
Services: services,
RemoveOrphans: createOpts.removeOrphans,
IgnoreOrphans: createOpts.ignoreOrphans,
Recreate: createOpts.recreateStrategy(),
RecreateDependencies: createOpts.dependenciesRecreateStrategy(),
Inherit: !createOpts.noInherit,
Timeout: createOpts.GetTimeout(),
QuietPull: false,
})
}

func (opts createOptions) recreateStrategy() string {
if opts.noRecreate {
return api.RecreateNever
Expand Down
170 changes: 170 additions & 0 deletions cmd/compose/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package compose

import (
"context"
"fmt"
"testing"

"github.com/compose-spec/compose-go/types"
"github.com/davecgh/go-spew/spew"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)

func TestRunCreate(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy(""),
deepEqual(defaultCreateOptions(true)),
)

createOpts := createOptions{}
buildOpts := buildOptions{}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}

func TestRunCreate_Build(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy("build"),
deepEqual(defaultCreateOptions(true)),
)

createOpts := createOptions{
Build: true,
}
buildOpts := buildOptions{}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}

func TestRunCreate_NoBuild(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy(""),
deepEqual(defaultCreateOptions(false)),
)

createOpts := createOptions{
noBuild: true,
}
buildOpts := buildOptions{}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}

func sampleProject() *types.Project {
return &types.Project{
Name: "test",
Services: types.Services{
{
Name: "svc",
Build: &types.BuildConfig{
Context: ".",
},
},
},
}
}

func defaultCreateOptions(includeBuild bool) api.CreateOptions {
var build *api.BuildOptions
if includeBuild {
bo := defaultBuildOptions()
build = &bo
}
return api.CreateOptions{
Build: build,
Services: nil,
RemoveOrphans: false,
IgnoreOrphans: false,
Recreate: "diverged",
RecreateDependencies: "diverged",
Inherit: true,
Timeout: nil,
QuietPull: false,
}
}

func defaultBuildOptions() api.BuildOptions {
return api.BuildOptions{
Args: make(types.MappingWithEquals),
Progress: "auto",
}
}

// deepEqual returns a nice diff on failure vs gomock.Eq when used
// on structs.
func deepEqual(x interface{}) gomock.Matcher {
return gomock.GotFormatterAdapter(
gomock.GotFormatterFunc(func(got interface{}) string {
return cmp.Diff(x, got)
}),
gomock.Eq(x),
)
}

func spewAdapter(m gomock.Matcher) gomock.Matcher {
return gomock.GotFormatterAdapter(
gomock.GotFormatterFunc(func(got interface{}) string {
return spew.Sdump(got)
}),
m,
)
}

type withPullPolicy struct {
policy string
}

func pullPolicy(policy string) gomock.Matcher {
return spewAdapter(withPullPolicy{policy: policy})
}

func (w withPullPolicy) Matches(x interface{}) bool {
proj, ok := x.(*types.Project)
if !ok || proj == nil || len(proj.Services) == 0 {
return false
}

for _, svc := range proj.Services {
if svc.PullPolicy != w.policy {
return false
}
}

return true
}

func (w withPullPolicy) String() string {
return fmt.Sprintf("has pull policy %q for all services", w.policy)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.7.3
github.com/cucumber/godog v0.0.0-00010101000000-000000000000 // replaced; see replace for the actual version used
github.com/davecgh/go-spew v1.1.1
github.com/distribution/reference v0.5.0
github.com/docker/buildx v0.11.2
github.com/docker/cli v24.0.5+incompatible
Expand All @@ -20,6 +21,7 @@ require (
github.com/docker/go-units v0.5.0
github.com/fsnotify/fsevents v0.1.1
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.9
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.6.0
github.com/jonboulle/clockwork v0.4.0
Expand Down Expand Up @@ -75,7 +77,6 @@ require (
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
github.com/cucumber/messages-go/v16 v16.0.1 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
Expand All @@ -94,7 +95,6 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
Expand Down

0 comments on commit 1311546

Please sign in to comment.