Merge pull request #1165 from docker/global-opts

Make `--file` and `--project-name` global compose options
This commit is contained in:
Nicolas De loof 2021-01-21 11:51:04 +01:00 committed by GitHub
commit 9e1ec76da8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 142 additions and 120 deletions

View File

@ -26,11 +26,14 @@ import (
)
type buildOptions struct {
*projectOptions
composeOptions
}
func buildCommand() *cobra.Command {
opts := buildOptions{}
func buildCommand(p *projectOptions) *cobra.Command {
opts := buildOptions{
projectOptions: p,
}
buildCmd := &cobra.Command{
Use: "build [SERVICE...]",
Short: "Build or rebuild services",
@ -38,9 +41,6 @@ func buildCommand() *cobra.Command {
return runBuild(cmd.Context(), opts, args)
},
}
buildCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
buildCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
return buildCmd
}

View File

@ -27,26 +27,22 @@ import (
"github.com/docker/compose-cli/api/context/store"
)
type composeOptions struct {
type projectOptions struct {
ProjectName string
DomainName string
WorkingDir string
ConfigPaths []string
Environment []string
WorkingDir string
EnvFile string
Format string
Detach bool
Build bool
Quiet bool
}
func addComposeCommonFlags(f *pflag.FlagSet, opts *composeOptions) {
f.StringVarP(&opts.ProjectName, "project-name", "p", "", "Project name")
f.StringVar(&opts.Format, "format", "", "Format the output. Values: [pretty | json]. (Default: pretty)")
f.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
func (o *projectOptions) addProjectFlags(f *pflag.FlagSet) {
f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
f.StringVar(&o.EnvFile, "env-file", "", "Specify an alternate environment file.")
f.StringVar(&o.WorkingDir, "workdir", "", "Specify an alternate working directory")
// TODO make --project-directory an alias
}
func (o *composeOptions) toProjectName() (string, error) {
func (o *projectOptions) toProjectName() (string, error) {
if o.ProjectName != "" {
return o.ProjectName, nil
}
@ -58,7 +54,7 @@ func (o *composeOptions) toProjectName() (string, error) {
return project.Name, nil
}
func (o *composeOptions) toProject() (*types.Project, error) {
func (o *projectOptions) toProject() (*types.Project, error) {
options, err := o.toProjectOptions()
if err != nil {
return nil, err
@ -71,18 +67,18 @@ func (o *composeOptions) toProject() (*types.Project, error) {
return project, nil
}
func (o *composeOptions) toProjectOptions() (*cli.ProjectOptions, error) {
func (o *projectOptions) toProjectOptions() (*cli.ProjectOptions, error) {
return cli.NewProjectOptions(o.ConfigPaths,
cli.WithOsEnv,
cli.WithEnvFile(o.EnvFile),
cli.WithDotEnv,
cli.WithEnv(o.Environment),
cli.WithWorkingDirectory(o.WorkingDir),
cli.WithName(o.ProjectName))
}
// Command returns the compose command with its child commands
func Command(contextType string) *cobra.Command {
opts := projectOptions{}
command := &cobra.Command{
Short: "Docker Compose",
Use: "compose",
@ -95,23 +91,24 @@ func Command(contextType string) *cobra.Command {
}
command.AddCommand(
upCommand(contextType),
downCommand(),
psCommand(),
upCommand(&opts, contextType),
downCommand(&opts),
psCommand(&opts),
listCommand(),
logsCommand(),
convertCommand(),
runCommand(),
logsCommand(&opts),
convertCommand(&opts),
runCommand(&opts),
)
if contextType == store.LocalContextType || contextType == store.DefaultContextType {
command.AddCommand(
buildCommand(),
pushCommand(),
pullCommand(),
buildCommand(&opts),
pushCommand(&opts),
pullCommand(&opts),
)
}
command.Flags().SetInterspersed(false)
opts.addProjectFlags(command.PersistentFlags())
return command
}

View File

@ -27,8 +27,15 @@ import (
"github.com/docker/compose-cli/api/client"
)
func convertCommand() *cobra.Command {
opts := composeOptions{}
type convertOptions struct {
*projectOptions
Format string
}
func convertCommand(p *projectOptions) *cobra.Command {
opts := convertOptions{
projectOptions: p,
}
convertCmd := &cobra.Command{
Use: "convert",
Short: "Converts the compose file to a cloud format (default: cloudformation)",
@ -36,17 +43,13 @@ func convertCommand() *cobra.Command {
return runConvert(cmd.Context(), opts)
},
}
convertCmd.Flags().StringVarP(&opts.ProjectName, "project-name", "p", "", "Project name")
convertCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
convertCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
convertCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables")
convertCmd.Flags().StringVar(&opts.EnvFile, "env-file", "", "Specify an alternate environment file.")
convertCmd.Flags().StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
flags := convertCmd.Flags()
flags.StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
return convertCmd
}
func runConvert(ctx context.Context, opts composeOptions) error {
func runConvert(ctx context.Context, opts convertOptions) error {
var json []byte
c, err := client.NewWithDefaultLocalBackend(ctx)
if err != nil {

View File

@ -29,8 +29,14 @@ import (
"github.com/docker/compose-cli/api/progress"
)
func downCommand() *cobra.Command {
opts := composeOptions{}
type downOptions struct {
*projectOptions
}
func downCommand(p *projectOptions) *cobra.Command {
opts := downOptions{
projectOptions: p,
}
downCmd := &cobra.Command{
Use: "down",
Short: "Stop and remove containers, networks",
@ -38,14 +44,10 @@ func downCommand() *cobra.Command {
return runDown(cmd.Context(), opts)
},
}
downCmd.Flags().StringVarP(&opts.ProjectName, "project-name", "p", "", "Project name")
downCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
downCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
return downCmd
}
func runDown(ctx context.Context, opts composeOptions) error {
func runDown(ctx context.Context, opts downOptions) error {
c, err := client.NewWithDefaultLocalBackend(ctx)
if err != nil {
return err

View File

@ -30,8 +30,13 @@ import (
"github.com/docker/compose-cli/cli/formatter"
)
type lsOptions struct {
Format string
Quiet bool
}
func listCommand() *cobra.Command {
opts := composeOptions{}
opts := lsOptions{}
lsCmd := &cobra.Command{
Use: "ls",
Short: "List running compose projects",
@ -39,16 +44,17 @@ func listCommand() *cobra.Command {
return runList(cmd.Context(), opts)
},
}
addComposeCommonFlags(lsCmd.Flags(), &opts)
lsCmd.Flags().StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json].")
lsCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
return lsCmd
}
func runList(ctx context.Context, opts composeOptions) error {
func runList(ctx context.Context, opts lsOptions) error {
c, err := client.NewWithDefaultLocalBackend(ctx)
if err != nil {
return err
}
stackList, err := c.ComposeService().List(ctx, opts.ProjectName)
stackList, err := c.ComposeService().List(ctx, "")
if err != nil {
return err
}

View File

@ -27,8 +27,15 @@ import (
"github.com/docker/compose-cli/cli/formatter"
)
func logsCommand() *cobra.Command {
opts := composeOptions{}
type logsOptions struct {
*projectOptions
composeOptions
}
func logsCommand(p *projectOptions) *cobra.Command {
opts := logsOptions{
projectOptions: p,
}
logsCmd := &cobra.Command{
Use: "logs [service...]",
Short: "View output from containers",
@ -36,14 +43,10 @@ func logsCommand() *cobra.Command {
return runLogs(cmd.Context(), opts, args)
},
}
logsCmd.Flags().StringVarP(&opts.ProjectName, "project-name", "p", "", "Project name")
logsCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
logsCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
return logsCmd
}
func runLogs(ctx context.Context, opts composeOptions, services []string) error {
func runLogs(ctx context.Context, opts logsOptions, services []string) error {
c, err := client.NewWithDefaultLocalBackend(ctx)
if err != nil {
return err

View File

@ -30,8 +30,16 @@ import (
"github.com/docker/compose-cli/cli/formatter"
)
func psCommand() *cobra.Command {
opts := composeOptions{}
type psOptions struct {
*projectOptions
Format string
Quiet bool
}
func psCommand(p *projectOptions) *cobra.Command {
opts := psOptions{
projectOptions: p,
}
psCmd := &cobra.Command{
Use: "ps",
Short: "List containers",
@ -39,13 +47,12 @@ func psCommand() *cobra.Command {
return runPs(cmd.Context(), opts)
},
}
psCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
psCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
addComposeCommonFlags(psCmd.Flags(), &opts)
psCmd.Flags().StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json].")
psCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
return psCmd
}
func runPs(ctx context.Context, opts composeOptions) error {
func runPs(ctx context.Context, opts psOptions) error {
c, err := client.NewWithDefaultLocalBackend(ctx)
if err != nil {
return err

View File

@ -26,11 +26,14 @@ import (
)
type pullOptions struct {
*projectOptions
composeOptions
}
func pullCommand() *cobra.Command {
opts := pullOptions{}
func pullCommand(p *projectOptions) *cobra.Command {
opts := pullOptions{
projectOptions: p,
}
pullCmd := &cobra.Command{
Use: "pull [SERVICE...]",
Short: "Pull service images",
@ -38,10 +41,6 @@ func pullCommand() *cobra.Command {
return runPull(cmd.Context(), opts, args)
},
}
pullCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
pullCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
return pullCmd
}

View File

@ -26,11 +26,14 @@ import (
)
type pushOptions struct {
*projectOptions
composeOptions
}
func pushCommand() *cobra.Command {
opts := pushOptions{}
func pushCommand(p *projectOptions) *cobra.Command {
opts := pushOptions{
projectOptions: p,
}
pushCmd := &cobra.Command{
Use: "push [SERVICE...]",
Short: "Push service images",
@ -38,10 +41,6 @@ func pushCommand() *cobra.Command {
return runPush(cmd.Context(), opts, args)
},
}
pushCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
pushCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
return pushCmd
}

View File

@ -29,18 +29,20 @@ import (
)
type runOptions struct {
Name string
*composeOptions
Service string
Command []string
WorkingDir string
ConfigPaths []string
Environment []string
Detach bool
Remove bool
}
func runCommand() *cobra.Command {
opts := runOptions{}
func runCommand(p *projectOptions) *cobra.Command {
opts := runOptions{
composeOptions: &composeOptions{
projectOptions: p,
},
}
runCmd := &cobra.Command{
Use: "run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...] SERVICE [COMMAND] [ARGS...]",
Short: "Run a one-off command on a service.",
@ -53,8 +55,6 @@ func runCommand() *cobra.Command {
return runRun(cmd.Context(), opts)
},
}
runCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
runCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
runCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
runCmd.Flags().StringArrayVarP(&opts.Environment, "env", "e", []string{}, "Set environment variables")
runCmd.Flags().BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits")
@ -64,12 +64,7 @@ func runCommand() *cobra.Command {
}
func runRun(ctx context.Context, opts runOptions) error {
projectOpts := composeOptions{
ConfigPaths: opts.ConfigPaths,
WorkingDir: opts.WorkingDir,
Environment: opts.Environment,
}
c, project, err := setup(ctx, projectOpts, []string{opts.Service})
c, project, err := setup(ctx, *opts.composeOptions, []string{opts.Service})
if err != nil {
return err
}

View File

@ -32,13 +32,27 @@ import (
"github.com/spf13/cobra"
)
// composeOptions hold options common to `up` and `run` to run compose project
type composeOptions struct {
*projectOptions
Build bool
// ACI only
DomainName string
}
type upOptions struct {
composeOptions
*composeOptions
Detach bool
Environment []string
removeOrphans bool
}
func upCommand(contextType string) *cobra.Command {
opts := upOptions{}
func upCommand(p *projectOptions, contextType string) *cobra.Command {
opts := upOptions{
composeOptions: &composeOptions{
projectOptions: p,
},
}
upCmd := &cobra.Command{
Use: "up [SERVICE...]",
Short: "Create and start containers",
@ -51,24 +65,21 @@ func upCommand(contextType string) *cobra.Command {
}
},
}
upCmd.Flags().StringVarP(&opts.ProjectName, "project-name", "p", "", "Project name")
upCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
upCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
upCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables")
upCmd.Flags().StringVar(&opts.EnvFile, "env-file", "", "Specify an alternate environment file.")
upCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
upCmd.Flags().BoolVar(&opts.Build, "build", false, "Build images before starting containers.")
upCmd.Flags().BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
flags := upCmd.Flags()
flags.StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables")
flags.BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.")
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
if contextType == store.AciContextType {
upCmd.Flags().StringVar(&opts.DomainName, "domainname", "", "Container NIS domain name")
flags.StringVar(&opts.DomainName, "domainname", "", "Container NIS domain name")
}
return upCmd
}
func runUp(ctx context.Context, opts upOptions, services []string) error {
c, project, err := setup(ctx, opts.composeOptions, services)
c, project, err := setup(ctx, *opts.composeOptions, services)
if err != nil {
return err
}
@ -82,7 +93,7 @@ func runUp(ctx context.Context, opts upOptions, services []string) error {
}
func runCreateStart(ctx context.Context, opts upOptions, services []string) error {
c, project, err := setup(ctx, opts.composeOptions, services)
c, project, err := setup(ctx, *opts.composeOptions, services)
if err != nil {
return err
}

View File

@ -50,11 +50,11 @@ func TestLocalComposeUp(t *testing.T) {
const projectName = "compose-e2e-demo"
t.Run("up", func(t *testing.T) {
c.RunDockerCmd("compose", "up", "-d", "-f", "./fixtures/sentences/docker-compose.yaml", "--project-name", projectName, "-d")
c.RunDockerCmd("compose", "-f", "./fixtures/sentences/compose.yaml", "--project-name", projectName, "up", "-d")
})
t.Run("check running project", func(t *testing.T) {
res := c.RunDockerCmd("compose", "ps", "-p", projectName)
res := c.RunDockerCmd("compose", "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `web`})
endpoint := "http://localhost:90"
@ -71,7 +71,7 @@ func TestLocalComposeUp(t *testing.T) {
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project": "compose-e2e-demo"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.oneoff": "False",`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.config-hash":`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project.config_files": "./fixtures/sentences/docker-compose.yaml"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project.config_files": "./fixtures/sentences/compose.yaml"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project.working_dir":`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.service": "web"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.version":`})
@ -89,7 +89,7 @@ func TestLocalComposeUp(t *testing.T) {
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerCmd("compose", "down", "--project-name", projectName)
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
})
t.Run("check containers after down", func(t *testing.T) {
@ -107,7 +107,7 @@ func TestLocalComposeRun(t *testing.T) {
c := NewParallelE2eCLI(t, binDir)
t.Run("compose run", func(t *testing.T) {
res := c.RunDockerCmd("compose", "run", "-f", "./fixtures/run-test/docker-compose.yml", "back")
res := c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yml", "run", "back")
lines := Lines(res.Stdout())
assert.Equal(t, lines[len(lines)-1], "Hello there!!", res.Stdout())
})
@ -141,7 +141,7 @@ func TestLocalComposeRun(t *testing.T) {
})
t.Run("compose run --rm", func(t *testing.T) {
res := c.RunDockerCmd("compose", "run", "-f", "./fixtures/run-test/docker-compose.yml", "--rm", "back", "/bin/sh", "-c", "echo Hello again")
res := c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yml", "run", "--rm", "back", "/bin/sh", "-c", "echo Hello again")
lines := Lines(res.Stdout())
assert.Equal(t, lines[len(lines)-1], "Hello again", res.Stdout())
})
@ -152,7 +152,7 @@ func TestLocalComposeRun(t *testing.T) {
})
t.Run("down", func(t *testing.T) {
c.RunDockerCmd("compose", "down", "-f", "./fixtures/run-test/docker-compose.yml")
c.RunDockerCmd("compose", "-f", "./fixtures/run-test/compose.yml", "down")
res := c.RunDockerCmd("ps", "--all")
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout())
})
@ -169,11 +169,11 @@ func TestNetworks(t *testing.T) {
})
t.Run("up", func(t *testing.T) {
c.RunDockerCmd("compose", "up", "-d", "-f", "./fixtures/network-test/docker-compose.yaml", "--project-name", projectName, "-d")
c.RunDockerCmd("compose", "-f", "./fixtures/network-test/compose.yaml", "--project-name", projectName, "up", "-d")
})
t.Run("check running project", func(t *testing.T) {
res := c.RunDockerCmd("compose", "ps", "-p", projectName)
res := c.RunDockerCmd("compose", "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `web`})
endpoint := "http://localhost:80"
@ -186,7 +186,7 @@ func TestNetworks(t *testing.T) {
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerCmd("compose", "down", "--project-name", projectName)
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
})
t.Run("check networks after down", func(t *testing.T) {
@ -204,7 +204,7 @@ func TestLocalComposeBuild(t *testing.T) {
c.RunDockerOrExitError("rmi", "build-test_nginx")
c.RunDockerOrExitError("rmi", "custom-nginx")
res := c.RunDockerCmd("compose", "build", "--workdir", "fixtures/build-test")
res := c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "build")
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
c.RunDockerCmd("image", "inspect", "build-test_nginx")
@ -215,9 +215,9 @@ func TestLocalComposeBuild(t *testing.T) {
c.RunDockerOrExitError("rmi", "build-test_nginx")
c.RunDockerOrExitError("rmi", "custom-nginx")
res := c.RunDockerCmd("compose", "up", "-d", "--workdir", "fixtures/build-test")
res := c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "up", "-d")
t.Cleanup(func() {
c.RunDockerCmd("compose", "down", "--workdir", "fixtures/build-test")
c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "down")
})
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
@ -230,13 +230,13 @@ func TestLocalComposeBuild(t *testing.T) {
})
t.Run("no rebuild when up again", func(t *testing.T) {
res := c.RunDockerCmd("compose", "up", "-d", "--workdir", "fixtures/build-test")
res := c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "up", "-d")
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static /usr/share/nginx/html"), res.Stdout())
})
t.Run("cleanup build project", func(t *testing.T) {
c.RunDockerCmd("compose", "down", "--workdir", "fixtures/build-test")
c.RunDockerCmd("compose", "--workdir", "fixtures/build-test", "down")
c.RunDockerCmd("rmi", "build-test_nginx")
c.RunDockerCmd("rmi", "custom-nginx")
})
@ -252,7 +252,7 @@ func TestLocalComposeVolume(t *testing.T) {
c.RunDockerOrExitError("rmi", "compose-e2e-volume_nginx")
c.RunDockerOrExitError("volume", "rm", projectName+"_staticVol")
c.RunDockerOrExitError("volume", "rm", "myvolume")
c.RunDockerCmd("compose", "up", "-d", "--workdir", "fixtures/volume-test", "--project-name", projectName)
c.RunDockerCmd("compose", "--workdir", "fixtures/volume-test", "--project-name", projectName, "up", "-d")
})
t.Run("access bind mount data", func(t *testing.T) {
@ -276,7 +276,7 @@ func TestLocalComposeVolume(t *testing.T) {
})
t.Run("cleanup volume project", func(t *testing.T) {
c.RunDockerCmd("compose", "down", "--project-name", projectName)
c.RunDockerCmd("compose", "--project-name", projectName, "down")
c.RunDockerCmd("volume", "rm", projectName+"_staticVol")
})
}
@ -284,7 +284,7 @@ func TestLocalComposeVolume(t *testing.T) {
func TestComposePull(t *testing.T) {
c := NewParallelE2eCLI(t, binDir)
res := c.RunDockerOrExitError("compose", "pull", "--workdir", "fixtures/simple-composefile")
res := c.RunDockerOrExitError("compose", "--workdir", "fixtures/simple-composefile", "pull")
output := res.Combined()
assert.Assert(t, strings.Contains(output, "simple Pulled"))