diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e645d59b7f..c3637bd466 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,7 +129,12 @@ jobs: name: coverage-data-unit path: bin/coverage/unit/ if-no-files-found: error - + - + name: Unit Test Summary + uses: test-summary/action@v2 + with: + paths: bin/coverage/unit/report.xml + if: always() e2e: runs-on: ubuntu-latest strategy: @@ -138,10 +143,20 @@ jobs: mode: - plugin - standalone + engine: + - 24.0.9 + - 25.0.3 steps: - name: Checkout uses: actions/checkout@v3 + - name: Install Docker ${{ matrix.engine }} + run: | + sudo apt-get install curl + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh ./get-docker.sh --version ${{ matrix.engine }} + - name: Check Docker Version + run: docker --version - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -152,11 +167,6 @@ jobs: go-version-file: 'go.mod' check-latest: true cache: true - - - name: Setup docker CLI - run: | - curl https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_CLI_VERSION}.tgz | tar xz - sudo cp ./docker/docker /usr/bin/ && rm -rf docker && docker version - name: Build uses: docker/bake-action@v2 @@ -197,7 +207,12 @@ jobs: rm -f /usr/local/bin/docker-compose cp bin/build/docker-compose /usr/local/bin make e2e-compose-standalone - + - + name: e2e Test Summary + uses: test-summary/action@v2 + with: + paths: /tmp/report/report.xml + if: always() coverage: runs-on: ubuntu-22.04 needs: diff --git a/Dockerfile b/Dockerfile index 51e4a52ffe..a6f74d07d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -ARG GO_VERSION=1.21.6 +ARG GO_VERSION=1.21.8 ARG XX_VERSION=1.2.1 ARG GOLANGCI_LINT_VERSION=v1.55.2 ARG ADDLICENSE_VERSION=v1.0.0 @@ -106,11 +106,14 @@ RUN --mount=type=bind,target=. \ --mount=type=cache,target=/go/pkg/mod \ rm -rf /tmp/coverage && \ mkdir -p /tmp/coverage && \ - go test -tags "$BUILD_TAGS" -v -cover -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') -args -test.gocoverdir="/tmp/coverage" && \ + rm -rf /tmp/report && \ + mkdir -p /tmp/report && \ + go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -tags "$BUILD_TAGS" -v -cover -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') -args -test.gocoverdir="/tmp/coverage" && \ go tool covdata percent -i=/tmp/coverage FROM scratch AS test-coverage COPY --from=test --link /tmp/coverage / +COPY --from=test --link /tmp/report / FROM base AS license-set ARG LICENSE_FILES diff --git a/Makefile b/Makefile index 2bce970904..df30dc8600 100644 --- a/Makefile +++ b/Makefile @@ -75,11 +75,11 @@ install: binary .PHONY: e2e-compose e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test - go test -v $(TEST_FLAGS) -count=1 ./pkg/e2e + go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 ./pkg/e2e .PHONY: e2e-compose-standalone e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test - go test $(TEST_FLAGS) -v -count=1 -parallel=1 --tags=standalone ./pkg/e2e + go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=1 --tags=standalone ./pkg/e2e .PHONY: build-and-e2e-compose build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test diff --git a/cmd/compose/attach.go b/cmd/compose/attach.go index 1899edef11..a4504a0aba 100644 --- a/cmd/compose/attach.go +++ b/cmd/compose/attach.go @@ -43,7 +43,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service } runCmd := &cobra.Command{ Use: "attach [OPTIONS] SERVICE", - Short: "Attach local standard input, output, and error streams to a service's running container.", + Short: "Attach local standard input, output, and error streams to a service's running container", Args: cobra.MinimumNArgs(1), PreRunE: Adapt(func(ctx context.Context, args []string) error { opts.service = args[0] @@ -64,7 +64,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service } func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error { - projectName, err := opts.toProjectName(dockerCli) + projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/build.go b/cmd/compose/build.go index 5f855c9220..a204c37a00 100644 --- a/cmd/compose/build.go +++ b/cmd/compose/build.go @@ -111,13 +111,13 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) ValidArgsFunction: completeServiceNames(dockerCli, p), } flags := cmd.Flags() - flags.BoolVar(&opts.push, "push", false, "Push service images.") + flags.BoolVar(&opts.push, "push", false, "Push service images") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT") - flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.") - flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services.") + flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image") + flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services") flags.StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)") - flags.StringVar(&opts.builder, "builder", "", "Set builder to use.") - flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively).") + flags.StringVar(&opts.builder, "builder", "", "Set builder to use") + flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively)") flags.Bool("parallel", true, "Build images in parallel. DEPRECATED") flags.MarkHidden("parallel") //nolint:errcheck @@ -136,7 +136,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error { - project, err := opts.ToProject(dockerCli, services, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution) + project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution) if err != nil { return err } diff --git a/cmd/compose/completion.go b/cmd/compose/completion.go index 83b233f1e2..d9c56b15da 100644 --- a/cmd/compose/completion.go +++ b/cmd/compose/completion.go @@ -37,17 +37,18 @@ func noCompletion() validArgsFn { func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { p.Offline = true - project, err := p.ToProject(dockerCli, nil) + project, _, err := p.ToProject(cmd.Context(), dockerCli, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } + var values []string serviceNames := append(project.ServiceNames(), project.DisabledServiceNames()...) for _, s := range serviceNames { if toComplete == "" || strings.HasPrefix(s, toComplete) { - serviceNames = append(serviceNames, s) + values = append(values, s) } } - return serviceNames, cobra.ShellCompDirectiveNoFileComp + return values, cobra.ShellCompDirectiveNoFileComp } } @@ -72,7 +73,7 @@ func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []s func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { p.Offline = true - project, err := p.ToProject(dockerCli, nil) + project, _, err := p.ToProject(cmd.Context(), dockerCli, nil) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 4921450b1f..8af123e517 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -29,24 +29,26 @@ import ( "github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/dotenv" + "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/types" composegoutils "github.com/compose-spec/compose-go/v2/utils" "github.com/docker/buildx/util/logutil" dockercli "github.com/docker/cli/cli" "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli/command" + "github.com/docker/compose/v2/cmd/formatter" + "github.com/docker/compose/v2/internal/desktop" + "github.com/docker/compose/v2/internal/tracing" + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/compose" + ui "github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/remote" + "github.com/docker/compose/v2/pkg/utils" buildkit "github.com/moby/buildkit/util/progress/progressui" "github.com/morikuni/aec" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" - - "github.com/docker/compose/v2/cmd/formatter" - "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/compose" - ui "github.com/docker/compose/v2/pkg/progress" - "github.com/docker/compose/v2/pkg/utils" ) const ( @@ -140,14 +142,15 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF options := []cli.ProjectOptionsFn{ cli.WithResolvedPaths(true), cli.WithDiscardEnvFile, - cli.WithContext(ctx), } - project, err := o.ToProject(dockerCli, args, options...) + project, metrics, err := o.ToProject(ctx, dockerCli, args, options...) if err != nil { return err } + ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics) + return fn(ctx, project, args) }) } @@ -156,7 +159,7 @@ func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) { f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable") f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name") f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files") - f.StringArrayVar(&o.EnvFiles, "env-file", nil, "Specify an alternate environment file.") + f.StringArrayVar(&o.EnvFiles, "env-file", nil, "Specify an alternate environment file") f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)") f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)") f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode") @@ -164,11 +167,11 @@ func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) { _ = f.MarkHidden("workdir") } -func (o *ProjectOptions) projectOrName(dockerCli command.Cli, services ...string) (*types.Project, string, error) { +func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cli, services ...string) (*types.Project, string, error) { name := o.ProjectName var project *types.Project if len(o.ConfigPaths) > 0 || o.ProjectName == "" { - p, err := o.ToProject(dockerCli, services, cli.WithDiscardEnvFile) + p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile) if err != nil { envProjectName := os.Getenv(ComposeProjectName) if envProjectName != "" { @@ -182,7 +185,7 @@ func (o *ProjectOptions) projectOrName(dockerCli command.Cli, services ...string return project, name, nil } -func (o *ProjectOptions) toProjectName(dockerCli command.Cli) (string, error) { +func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cli) (string, error) { if o.ProjectName != "" { return o.ProjectName, nil } @@ -192,39 +195,83 @@ func (o *ProjectOptions) toProjectName(dockerCli command.Cli) (string, error) { return envProjectName, nil } - project, err := o.ToProject(dockerCli, nil) + project, _, err := o.ToProject(ctx, dockerCli, nil) if err != nil { return "", err } return project.Name, nil } -func (o *ProjectOptions) ToProject(dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) { - if !o.Offline { - po = o.configureRemoteLoaders(dockerCli, po) +func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) { + remotes := o.remoteLoaders(dockerCli) + for _, r := range remotes { + po = append(po, cli.WithResourceLoader(r)) } options, err := o.toProjectOptions(po...) if err != nil { - return nil, compose.WrapComposeError(err) + return nil, err } if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) { api.Separator = "_" } - project, err := cli.ProjectFromOptions(options) + return options.LoadModel(ctx) +} + +func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { + var metrics tracing.Metrics + + remotes := o.remoteLoaders(dockerCli) + for _, r := range remotes { + po = append(po, cli.WithResourceLoader(r)) + } + + options, err := o.toProjectOptions(po...) + if err != nil { + return nil, metrics, compose.WrapComposeError(err) + } + + options.WithListeners(func(event string, metadata map[string]any) { + switch event { + case "extends": + metrics.CountExtends++ + case "include": + paths := metadata["path"].(types.StringList) + for _, path := range paths { + var isRemote bool + for _, r := range remotes { + if r.Accept(path) { + isRemote = true + break + } + } + if isRemote { + metrics.CountIncludesRemote++ + } else { + metrics.CountIncludesLocal++ + } + } + } + }) + + if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) { + api.Separator = "_" + } + + project, err := options.LoadProject(ctx) if err != nil { - return nil, compose.WrapComposeError(err) + return nil, metrics, compose.WrapComposeError(err) } if project.Name == "" { - return nil, errors.New("project name can't be empty. Use `--project-name` to set a valid name") + return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name") } project, err = project.WithServicesEnabled(services...) if err != nil { - return nil, err + return nil, metrics, err } for name, s := range project.Services { @@ -245,15 +292,16 @@ func (o *ProjectOptions) ToProject(dockerCli command.Cli, services []string, po project = project.WithoutUnnecessaryResources() project, err = project.WithSelectedServices(services) - return project, err + return project, metrics, err } -func (o *ProjectOptions) configureRemoteLoaders(dockerCli command.Cli, po []cli.ProjectOptionsFn) []cli.ProjectOptionsFn { +func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader { + if o.Offline { + return nil + } git := remote.NewGitRemoteLoader(o.Offline) oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline) - - po = append(po, cli.WithResourceLoader(git), cli.WithResourceLoader(oci)) - return po + return []loader.ResourceLoader{git, oci} } func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) { @@ -261,10 +309,10 @@ func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.Proj append(po, cli.WithWorkingDirectory(o.ProjectDir), cli.WithOsEnv, - cli.WithEnvFiles(o.EnvFiles...), - cli.WithDotEnv, cli.WithConfigFileEnv, cli.WithDefaultConfigPath, + cli.WithEnvFiles(o.EnvFiles...), + cli.WithDotEnv, cli.WithDefaultProfiles(o.Profiles...), cli.WithName(o.ProjectName))...) } @@ -300,7 +348,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // ) c := &cobra.Command{ Short: "Docker Compose", - Long: "Define and run multi-container applications with Docker.", + Long: "Define and run multi-container applications with Docker", Use: PluginName, TraverseChildren: true, // By default (no Run/RunE in parent c) for typos in subcommands, cobra displays the help of parent c but exit(0) ! @@ -318,11 +366,17 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // } }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + // (1) process env vars err := setEnvWithDotEnv(&opts) if err != nil { return err } parent := cmd.Root() + + // (2) call parent pre-run + // TODO(milas): this seems incorrect, remove or document if parent != nil { parentPrerun := parent.PersistentPreRunE if parentPrerun != nil { @@ -332,6 +386,11 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // } } } + + // (3) set up display/output + if verbose { + logrus.SetLevel(logrus.TraceLevel) + } if noAnsi { if ansi != "auto" { return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`) @@ -339,14 +398,9 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // ansi = "never" fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n") } - if verbose { - logrus.SetLevel(logrus.TraceLevel) - } - if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") { ansi = v } - formatter.SetANSIMode(dockerCli, ansi) if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" { @@ -364,6 +418,9 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // switch opts.Progress { case ui.ModeAuto: ui.Mode = ui.ModeAuto + if ansi == "never" { + ui.Mode = ui.ModePlain + } case ui.ModeTTY: if ansi == "never" { return fmt.Errorf("can't use --progress tty while ANSI support is disabled") @@ -380,6 +437,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // return fmt.Errorf("unsupported --progress value %q", opts.Progress) } + // (4) options validation / normalization if opts.WorkDir != "" { if opts.ProjectDir != "" { return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`) @@ -416,13 +474,26 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // parallel = i } if parallel > 0 { + logrus.Debugf("Limiting max concurrency to %d jobs", parallel) backend.MaxConcurrency(parallel) } - ctx, err := backend.DryRunMode(cmd.Context(), dryRun) + + // (5) dry run detection + ctx, err = backend.DryRunMode(ctx, dryRun) if err != nil { return err } cmd.SetContext(ctx) + + // (6) Desktop integration + if db, ok := backend.(desktop.IntegrationService); ok { + if err := db.MaybeEnableDesktopIntegration(ctx); err != nil { + // not fatal, Compose will still work but behave as though + // it's not running as part of Docker Desktop + logrus.Debugf("failed to enable Docker Desktop integration: %v", err) + } + } + return nil }, } @@ -436,7 +507,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // psCommand(&opts, dockerCli, backend), listCommand(dockerCli, backend), logsCommand(&opts, dockerCli, backend), - configCommand(&opts, dockerCli, backend), + configCommand(&opts, dockerCli), killCommand(&opts, dockerCli, backend), runCommand(&opts, dockerCli, backend), removeCommand(&opts, dockerCli, backend), diff --git a/cmd/compose/config.go b/cmd/compose/config.go index cf26832cfd..b06e620b11 100644 --- a/cmd/compose/config.go +++ b/cmd/compose/config.go @@ -19,6 +19,7 @@ package compose import ( "bytes" "context" + "encoding/json" "fmt" "os" "sort" @@ -28,6 +29,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/compose" @@ -51,18 +53,28 @@ type configOptions struct { } func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) { - po = append(po, + po = append(po, o.ToProjectOptions()...) + project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, services, po...) + return project, err +} + +func (o *configOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) { + po = append(po, o.ToProjectOptions()...) + return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...) +} + +func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn { + return []cli.ProjectOptionsFn{ cli.WithInterpolation(!o.noInterpolate), cli.WithResolvedPaths(!o.noResolvePath), cli.WithNormalization(!o.noNormalize), cli.WithConsistency(!o.noConsistency), cli.WithDefaultProfiles(o.Profiles...), cli.WithDiscardEnvFile, - cli.WithContext(ctx)) - return o.ProjectOptions.ToProject(dockerCli, services, po...) + } } -func configCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { opts := configOptions{ ProjectOptions: p, } @@ -100,43 +112,58 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service return runConfigImages(ctx, dockerCli, opts, args) } - return runConfig(ctx, dockerCli, backend, opts, args) + return runConfig(ctx, dockerCli, opts, args) }), ValidArgsFunction: completeServiceNames(dockerCli, p), } flags := cmd.Flags() flags.StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]") - flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests.") - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything.") - flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables.") - flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model.") - flags.BoolVar(&opts.noResolvePath, "no-path-resolution", false, "Don't resolve file paths.") + flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything") + flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables") + flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model") + flags.BoolVar(&opts.noResolvePath, "no-path-resolution", false, "Don't resolve file paths") flags.BoolVar(&opts.noConsistency, "no-consistency", false, "Don't check model consistency - warning: may produce invalid Compose output") - flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.") - flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.") - flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.") - flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.") - flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.") + flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line") + flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line") + flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line") + flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line") + flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line") flags.StringVarP(&opts.Output, "output", "o", "", "Save to file (default to stdout)") return cmd } -func runConfig(ctx context.Context, dockerCli command.Cli, backend api.Service, opts configOptions, services []string) error { +func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error { var content []byte - project, err := opts.ToProject(ctx, dockerCli, services) - if err != nil { - return err - } + if opts.noInterpolate { + // we can't use ToProject, so the model we render here is only partially resolved + model, err := opts.ToModel(ctx, dockerCli, services) + if err != nil { + return err + } - content, err = backend.Config(ctx, project, api.ConfigOptions{ - Format: opts.Format, - Output: opts.Output, - ResolveImageDigests: opts.resolveImageDigests, - }) - if err != nil { - return err + if opts.resolveImageDigests { + err = resolveImageDigests(ctx, dockerCli, model) + if err != nil { + return err + } + } + + content, err = formatModel(model, opts.Format) + if err != nil { + return err + } + } else { + project, err := opts.ToProject(ctx, dockerCli, services) + if err != nil { + return err + } + content, err = project.MarshalYAML() + if err != nil { + return err + } } if !opts.noInterpolate { @@ -150,10 +177,59 @@ func runConfig(ctx context.Context, dockerCli command.Cli, backend api.Service, if opts.Output != "" && len(content) > 0 { return os.WriteFile(opts.Output, content, 0o666) } - _, err = fmt.Fprint(dockerCli.Out(), string(content)) + _, err := fmt.Fprint(dockerCli.Out(), string(content)) return err } +func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[string]any) (err error) { + // create a pseudo-project so we can rely on WithImagesResolved to resolve images + p := &types.Project{ + Services: types.Services{}, + } + services := model["services"].(map[string]any) + for name, s := range services { + service := s.(map[string]any) + if image, ok := service["image"]; ok { + p.Services[name] = types.ServiceConfig{ + Image: image.(string), + } + } + } + + p, err = p.WithImagesResolved(compose.ImageDigestResolver(ctx, dockerCli.ConfigFile(), dockerCli.Client())) + if err != nil { + return err + } + + // Collect image resolved with digest and update model accordingly + for name, s := range services { + service := s.(map[string]any) + config := p.Services[name] + if config.Image != "" { + service["image"] = config.Image + } + services[name] = service + } + model["services"] = services + return nil +} + +func formatModel(model map[string]any, format string) (content []byte, err error) { + switch format { + case "json": + content, err = json.MarshalIndent(model, "", " ") + case "yaml": + buf := bytes.NewBuffer([]byte{}) + encoder := yaml.NewEncoder(buf) + encoder.SetIndent(2) + err = encoder.Encode(model) + content = buf.Bytes() + default: + return nil, fmt.Errorf("unsupported format %q", format) + } + return +} + func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error { project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution) if err != nil { diff --git a/cmd/compose/cp.go b/cmd/compose/cp.go index 4c346b2a7b..9cd07e5f77 100644 --- a/cmd/compose/cp.go +++ b/cmd/compose/cp.go @@ -65,10 +65,10 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } flags := copyCmd.Flags() - flags.IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas") - flags.BoolVar(&opts.all, "all", false, "copy to all the containers of the service.") - flags.MarkHidden("all") //nolint:errcheck - flags.MarkDeprecated("all", "by default all the containers of the service will get the source file/directory to be copied.") //nolint:errcheck + flags.IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas") + flags.BoolVar(&opts.all, "all", false, "Copy to all the containers of the service") + flags.MarkHidden("all") //nolint:errcheck + flags.MarkDeprecated("all", "By default all the containers of the service will get the source file/directory to be copied") //nolint:errcheck flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)") @@ -76,7 +76,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error { - name, err := opts.toProjectName(dockerCli) + name, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/create.go b/cmd/compose/create.go index a7731bae59..bf21ee7afc 100644 --- a/cmd/compose/create.go +++ b/cmd/compose/create.go @@ -55,7 +55,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service } cmd := &cobra.Command{ Use: "create [OPTIONS] [SERVICE...]", - Short: "Creates containers for a service.", + Short: "Creates containers for a service", PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { opts.pullChanged = cmd.Flags().Changed("pull") if opts.Build && opts.noBuild { @@ -72,12 +72,13 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service ValidArgsFunction: completeServiceNames(dockerCli, p), } flags := cmd.Flags() - flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.") - flags.BoolVar(&opts.noBuild, "no-build", false, "Don't build an image, even if it's policy.") + flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers") + flags.BoolVar(&opts.noBuild, "no-build", false, "Don't build an image, even if it's policy") flags.StringVar(&opts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never"|"build")`) - flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.") + flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information") + flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed") flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") - flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") + flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file") flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") return cmd } @@ -105,7 +106,7 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp RecreateDependencies: createOpts.dependenciesRecreateStrategy(), Inherit: !createOpts.noInherit, Timeout: createOpts.GetTimeout(), - QuietPull: false, + QuietPull: createOpts.quietPull, }) } diff --git a/cmd/compose/down.go b/cmd/compose/down.go index 97215a7857..d3080ed2df 100644 --- a/cmd/compose/down.go +++ b/cmd/compose/down.go @@ -63,9 +63,9 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } flags := downCmd.Flags() removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans)) - flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.") + flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file") flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds") - flags.BoolVarP(&opts.volumes, "volumes", "v", false, `Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers.`) + flags.BoolVarP(&opts.volumes, "volumes", "v", false, `Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers`) flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`) flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { if name == "volume" { @@ -78,7 +78,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } diff --git a/cmd/compose/events.go b/cmd/compose/events.go index 10ef5b4427..cade77a7fd 100644 --- a/cmd/compose/events.go +++ b/cmd/compose/events.go @@ -40,7 +40,7 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service } cmd := &cobra.Command{ Use: "events [OPTIONS] [SERVICE...]", - Short: "Receive real time events from containers.", + Short: "Receive real time events from containers", RunE: Adapt(func(ctx context.Context, args []string) error { return runEvents(ctx, dockerCli, backend, opts, args) }), @@ -52,7 +52,7 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service } func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error { - name, err := opts.toProjectName(dockerCli) + name, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/exec.go b/cmd/compose/exec.go index 12ebc70050..aa6774d5d5 100644 --- a/cmd/compose/exec.go +++ b/cmd/compose/exec.go @@ -51,7 +51,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } runCmd := &cobra.Command{ Use: "exec [OPTIONS] SERVICE COMMAND [ARGS...]", - Short: "Execute a command in a running container.", + Short: "Execute a command in a running container", Args: cobra.MinimumNArgs(2), PreRunE: Adapt(func(ctx context.Context, args []string) error { opts.service = args[0] @@ -64,17 +64,17 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) ValidArgsFunction: completeServiceNames(dockerCli, p), } - runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background.") + runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background") runCmd.Flags().StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables") - runCmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas") - runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process.") - runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user.") + runCmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas") + runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process") + runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user") runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.") - runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command.") + runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command") - runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.") + runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached") runCmd.Flags().MarkHidden("interactive") //nolint:errcheck - runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY.") + runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY") runCmd.Flags().MarkHidden("tty") //nolint:errcheck runCmd.Flags().SetInterspersed(false) @@ -82,7 +82,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error { - projectName, err := opts.toProjectName(dockerCli) + projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/images.go b/cmd/compose/images.go index d10700767a..8c68a34af3 100644 --- a/cmd/compose/images.go +++ b/cmd/compose/images.go @@ -51,13 +51,13 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service }), ValidArgsFunction: completeServiceNames(dockerCli, p), } - imgCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json].") + imgCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]") imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") return imgCmd } func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error { - projectName, err := opts.toProjectName(dockerCli) + projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/kill.go b/cmd/compose/kill.go index d38201cbc9..c0faa75c60 100644 --- a/cmd/compose/kill.go +++ b/cmd/compose/kill.go @@ -39,7 +39,7 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } cmd := &cobra.Command{ Use: "kill [OPTIONS] [SERVICE...]", - Short: "Force stop service containers.", + Short: "Force stop service containers", RunE: Adapt(func(ctx context.Context, args []string) error { return runKill(ctx, dockerCli, backend, opts, args) }), @@ -48,14 +48,14 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) flags := cmd.Flags() removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans)) - flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.") - flags.StringVarP(&opts.signal, "signal", "s", "SIGKILL", "SIGNAL to send to the container.") + flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file") + flags.StringVarP(&opts.signal, "signal", "s", "SIGKILL", "SIGNAL to send to the container") return cmd } func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } diff --git a/cmd/compose/list.go b/cmd/compose/list.go index e54bddb88e..bb1faeb12c 100644 --- a/cmd/compose/list.go +++ b/cmd/compose/list.go @@ -49,9 +49,9 @@ func listCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { Args: cobra.NoArgs, ValidArgsFunction: noCompletion(), } - lsCmd.Flags().StringVar(&lsOpts.Format, "format", "table", "Format the output. Values: [table | json].") - lsCmd.Flags().BoolVarP(&lsOpts.Quiet, "quiet", "q", false, "Only display IDs.") - lsCmd.Flags().Var(&lsOpts.Filter, "filter", "Filter output based on conditions provided.") + lsCmd.Flags().StringVar(&lsOpts.Format, "format", "table", "Format the output. Values: [table | json]") + lsCmd.Flags().BoolVarP(&lsOpts.Quiet, "quiet", "q", false, "Only display IDs") + lsCmd.Flags().Var(&lsOpts.Filter, "filter", "Filter output based on conditions provided") lsCmd.Flags().BoolVarP(&lsOpts.All, "all", "a", false, "Show all stopped Compose projects") return lsCmd diff --git a/cmd/compose/logs.go b/cmd/compose/logs.go index 2df7bb59f1..7661b02aed 100644 --- a/cmd/compose/logs.go +++ b/cmd/compose/logs.go @@ -59,22 +59,32 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) ValidArgsFunction: completeServiceNames(dockerCli, p), } flags := logsCmd.Flags() - flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output.") + flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") flags.IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas") flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") - flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output.") - flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.") - flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps.") - flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs for each container.") + flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output") + flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs") + flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") + flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs for each container") return logsCmd } func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } + + // exclude services configured to ignore output (attach: false), until explicitly selected + if project != nil && len(services) == 0 { + for n, service := range project.Services { + if service.Attach == nil || *service.Attach { + services = append(services, n) + } + } + } + consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false) return backend.Logs(ctx, name, consumer, api.LogOptions{ Project: project, diff --git a/cmd/compose/pause.go b/cmd/compose/pause.go index acfc0dec51..6f34577192 100644 --- a/cmd/compose/pause.go +++ b/cmd/compose/pause.go @@ -45,7 +45,7 @@ func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } @@ -76,7 +76,7 @@ func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic } func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } diff --git a/cmd/compose/port.go b/cmd/compose/port.go index 0baa875c3c..59ea8ef1ce 100644 --- a/cmd/compose/port.go +++ b/cmd/compose/port.go @@ -41,7 +41,7 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } cmd := &cobra.Command{ Use: "port [OPTIONS] SERVICE PRIVATE_PORT", - Short: "Print the public port for a port binding.", + Short: "Print the public port for a port binding", Args: cobra.MinimumNArgs(2), PreRunE: Adapt(func(ctx context.Context, args []string) error { port, err := strconv.ParseUint(args[1], 10, 16) @@ -58,12 +58,12 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) ValidArgsFunction: completeServiceNames(dockerCli, p), } cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp") - cmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas") + cmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas") return cmd } func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error { - projectName, err := opts.toProjectName(dockerCli) + projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go index 8b3f3d34d6..b1338837a7 100644 --- a/cmd/compose/ps.go +++ b/cmd/compose/ps.go @@ -81,7 +81,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c } flags := psCmd.Flags() flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp) - flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status).") + flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status)") flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]") flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") flags.BoolVar(&opts.Services, "services", false, "Display services") @@ -92,7 +92,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c } func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } @@ -113,7 +113,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv containers, err := backend.Ps(ctx, name, api.PsOptions{ Project: project, - All: opts.All, + All: opts.All || len(opts.Status) != 0, Services: services, }) if err != nil { diff --git a/cmd/compose/publish.go b/cmd/compose/publish.go index 2ef14aabf0..42b0140b58 100644 --- a/cmd/compose/publish.go +++ b/cmd/compose/publish.go @@ -44,13 +44,13 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic Args: cobra.ExactArgs(1), } flags := cmd.Flags() - flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests.") + flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests") flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI Image/Artifact specification version (automatically determined by default)") return cmd } func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error { - project, err := opts.ToProject(dockerCli, nil) + project, _, err := opts.ToProject(ctx, dockerCli, nil) if err != nil { return err } diff --git a/cmd/compose/pull.go b/cmd/compose/pull.go index d6ce561be7..f003fc19e9 100644 --- a/cmd/compose/pull.go +++ b/cmd/compose/pull.go @@ -60,15 +60,15 @@ func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) ValidArgsFunction: completeServiceNames(dockerCli, p), } flags := cmd.Flags() - flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information.") - cmd.Flags().BoolVar(&opts.includeDeps, "include-deps", false, "Also pull services declared as dependencies.") - cmd.Flags().BoolVar(&opts.parallel, "parallel", true, "DEPRECATED pull multiple images in parallel.") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information") + cmd.Flags().BoolVar(&opts.includeDeps, "include-deps", false, "Also pull services declared as dependencies") + cmd.Flags().BoolVar(&opts.parallel, "parallel", true, "DEPRECATED pull multiple images in parallel") flags.MarkHidden("parallel") //nolint:errcheck - cmd.Flags().BoolVar(&opts.parallel, "no-parallel", true, "DEPRECATED disable parallel pulling.") + cmd.Flags().BoolVar(&opts.parallel, "no-parallel", true, "DEPRECATED disable parallel pulling") flags.MarkHidden("no-parallel") //nolint:errcheck - cmd.Flags().BoolVar(&opts.ignorePullFailures, "ignore-pull-failures", false, "Pull what it can and ignores images with pull failures.") - cmd.Flags().BoolVar(&opts.noBuildable, "ignore-buildable", false, "Ignore images that can be built.") - cmd.Flags().StringVar(&opts.policy, "policy", "", `Apply pull policy ("missing"|"always").`) + cmd.Flags().BoolVar(&opts.ignorePullFailures, "ignore-pull-failures", false, "Pull what it can and ignores images with pull failures") + cmd.Flags().BoolVar(&opts.noBuildable, "ignore-buildable", false, "Ignore images that can be built") + cmd.Flags().StringVar(&opts.policy, "policy", "", `Apply pull policy ("missing"|"always")`) return cmd } @@ -94,7 +94,7 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types } func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error { - project, err := opts.ToProject(dockerCli, services) + project, _, err := opts.ToProject(ctx, dockerCli, services) if err != nil { return err } diff --git a/cmd/compose/push.go b/cmd/compose/push.go index ccc0e59917..177f9f2ec7 100644 --- a/cmd/compose/push.go +++ b/cmd/compose/push.go @@ -54,7 +54,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error { - project, err := opts.ToProject(dockerCli, services) + project, _, err := opts.ToProject(ctx, dockerCli, services) if err != nil { return err } diff --git a/cmd/compose/remove.go b/cmd/compose/remove.go index 46ac7b13fe..adcd3663c3 100644 --- a/cmd/compose/remove.go +++ b/cmd/compose/remove.go @@ -60,7 +60,7 @@ Any data which is not in a volume will be lost.`, } func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } diff --git a/cmd/compose/restart.go b/cmd/compose/restart.go index 9fe65f4615..718e15196f 100644 --- a/cmd/compose/restart.go +++ b/cmd/compose/restart.go @@ -50,13 +50,13 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic } flags := restartCmd.Flags() flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds") - flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't restart dependent services.") + flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't restart dependent services") return restartCmd } func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli) + project, name, err := opts.projectOrName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/run.go b/cmd/compose/run.go index c2649d5f12..b4a5da365f 100644 --- a/cmd/compose/run.go +++ b/cmd/compose/run.go @@ -129,7 +129,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * } cmd := &cobra.Command{ Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]", - Short: "Run a one-off command on a service.", + Short: "Run a one-off command on a service", Args: cobra.MinimumNArgs(1), PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { options.Service = args[0] @@ -156,7 +156,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * return nil }), RunE: Adapt(func(ctx context.Context, args []string) error { - project, err := p.ToProject(dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile) + project, _, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile) if err != nil { return err } @@ -175,24 +175,24 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables") flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label") flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits") - flags.BoolVarP(&options.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).") + flags.BoolVarP(&options.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected)") flags.StringVar(&options.name, "name", "", "Assign a name to the container") flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid") flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container") flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image") flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities") flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities") - flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services.") - flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume.") - flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.") - flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.") - flags.BoolVarP(&options.servicePorts, "service-ports", "P", false, "Run command with all service's ports enabled and mapped to the host.") - flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information.") - flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container.") - flags.BoolVar(&createOpts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") - - cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.") - cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY.") + flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services") + flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume") + flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host") + flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to") + flags.BoolVarP(&options.servicePorts, "service-ports", "P", false, "Run command with all service's ports enabled and mapped to the host") + flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information") + flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container") + flags.BoolVar(&createOpts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file") + + cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached") + cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY") cmd.Flags().MarkHidden("tty") //nolint:errcheck flags.SetNormalizeFunc(normalizeRunFlags) @@ -231,7 +231,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op } buildForDeps = &bo } - return startDependencies(ctx, backend, *project, buildForDeps, options.Service, options.ignoreOrphans) + return startDependencies(ctx, backend, *project, buildForDeps, options) }, dockerCli.Err()) if err != nil { return err @@ -298,11 +298,11 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op return err } -func startDependencies(ctx context.Context, backend api.Service, project types.Project, buildOpts *api.BuildOptions, requestedServiceName string, ignoreOrphans bool) error { +func startDependencies(ctx context.Context, backend api.Service, project types.Project, buildOpts *api.BuildOptions, options runOptions) error { dependencies := types.Services{} var requestedService types.ServiceConfig for name, service := range project.Services { - if name != requestedServiceName { + if name != options.Service { dependencies[name] = service } else { requestedService = service @@ -310,10 +310,11 @@ func startDependencies(ctx context.Context, backend api.Service, project types.P } project.Services = dependencies - project.DisabledServices[requestedServiceName] = requestedService + project.DisabledServices[options.Service] = requestedService err := backend.Create(ctx, &project, api.CreateOptions{ Build: buildOpts, - IgnoreOrphans: ignoreOrphans, + IgnoreOrphans: options.ignoreOrphans, + QuietPull: options.quietPull, }) if err != nil { return err diff --git a/cmd/compose/scale.go b/cmd/compose/scale.go index 2ab4351277..22e8e71dd4 100644 --- a/cmd/compose/scale.go +++ b/cmd/compose/scale.go @@ -54,14 +54,14 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) ValidArgsFunction: completeServiceNames(dockerCli, p), } flags := scaleCmd.Flags() - flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.") + flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services") return scaleCmd } func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error { services := maps.Keys(serviceReplicaTuples) - project, err := opts.ToProject(dockerCli, services) + project, _, err := opts.ToProject(ctx, dockerCli, services) if err != nil { return err } diff --git a/cmd/compose/start.go b/cmd/compose/start.go index 682a789065..6bde4b104b 100644 --- a/cmd/compose/start.go +++ b/cmd/compose/start.go @@ -44,7 +44,7 @@ func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } diff --git a/cmd/compose/stats.go b/cmd/compose/stats.go index c6c2f7fed0..2e503fe5db 100644 --- a/cmd/compose/stats.go +++ b/cmd/compose/stats.go @@ -63,7 +63,7 @@ Refer to https://docs.docker.com/go/formatting/ for more information about forma } func runStats(ctx context.Context, dockerCli command.Cli, opts statsOptions, service []string) error { - name, err := opts.ProjectOptions.toProjectName(dockerCli) + name, err := opts.ProjectOptions.toProjectName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/stop.go b/cmd/compose/stop.go index 2818ef4f33..d06cf6f229 100644 --- a/cmd/compose/stop.go +++ b/cmd/compose/stop.go @@ -54,7 +54,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error { - project, name, err := opts.projectOrName(dockerCli, services...) + project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } diff --git a/cmd/compose/top.go b/cmd/compose/top.go index fb7dd30d41..9d84c57a95 100644 --- a/cmd/compose/top.go +++ b/cmd/compose/top.go @@ -50,7 +50,7 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * } func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error { - projectName, err := opts.toProjectName(dockerCli) + projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } diff --git a/cmd/compose/up.go b/cmd/compose/up.go index c4919ca80e..084e010fad 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -20,14 +20,14 @@ import ( "context" "errors" "fmt" + "os" "strings" "time" - xprogress "github.com/moby/buildkit/util/progress/progressui" - "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/cmd/formatter" + xprogress "github.com/moby/buildkit/util/progress/progressui" "github.com/spf13/cobra" "github.com/docker/compose/v2/pkg/api" @@ -54,6 +54,7 @@ type upOptions struct { timestamp bool wait bool waitTimeout int + watch bool } func (opts upOptions) apply(project *types.Project, services []string) (*types.Project, error) { @@ -101,29 +102,31 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c } flags := upCmd.Flags() flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background") - flags.BoolVar(&create.Build, "build", false, "Build images before starting containers.") - flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's policy.") + flags.BoolVar(&create.Build, "build", false, "Build images before starting containers") + flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's policy") flags.StringVar(&create.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`) - flags.BoolVar(&create.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") + removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans)) + flags.BoolVar(&create.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file") flags.StringArrayVar(&create.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") - flags.BoolVar(&up.noColor, "no-color", false, "Produce monochrome output.") - flags.BoolVar(&up.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.") - flags.BoolVar(&create.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.") + flags.BoolVar(&up.noColor, "no-color", false, "Produce monochrome output") + flags.BoolVar(&up.noPrefix, "no-log-prefix", false, "Don't print prefix in logs") + flags.BoolVar(&create.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed") flags.BoolVar(&create.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.") - flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them.") + flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them") flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d") flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit") - flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running.") - flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps.") - flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services.") + flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running") + flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps") + flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services") flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.") - flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers.") - flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information.") + flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers") + flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information") flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.") - flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services.") - flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services.") + flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services") + flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services") flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.") - flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration to wait for the project to be running|healthy.") + flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration to wait for the project to be running|healthy") + flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.") return upCmd } @@ -255,6 +258,7 @@ func runUp( CascadeStop: upOptions.cascadeStop, Wait: upOptions.wait, WaitTimeout: timeout, + Watch: upOptions.watch, Services: services, }, }) diff --git a/cmd/compose/version.go b/cmd/compose/version.go index 8ca1ed57b0..e044043478 100644 --- a/cmd/compose/version.go +++ b/cmd/compose/version.go @@ -52,7 +52,7 @@ func versionCommand(dockerCli command.Cli) *cobra.Command { // define flags for backward compatibility with com.docker.cli flags := cmd.Flags() flags.StringVarP(&opts.format, "format", "f", "", "Format the output. Values: [pretty | json]. (Default: pretty)") - flags.BoolVar(&opts.short, "short", false, "Shows only Compose's version number.") + flags.BoolVar(&opts.short, "short", false, "Shows only Compose's version number") return cmd } diff --git a/cmd/compose/viz.go b/cmd/compose/viz.go index 2b4d650739..d97504e382 100644 --- a/cmd/compose/viz.go +++ b/cmd/compose/viz.go @@ -65,7 +65,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error { _, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL") - project, err := opts.ToProject(dockerCli, nil) + project, _, err := opts.ToProject(ctx, dockerCli, nil) if err != nil { return err } diff --git a/cmd/compose/wait.go b/cmd/compose/wait.go index e95818b959..88c9391400 100644 --- a/cmd/compose/wait.go +++ b/cmd/compose/wait.go @@ -61,7 +61,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) } func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) { - _, name, err := opts.projectOrName(dockerCli) + _, name, err := opts.projectOrName(ctx, dockerCli) if err != nil { return 0, err } diff --git a/cmd/compose/watch.go b/cmd/compose/watch.go index 24d704570e..880711a205 100644 --- a/cmd/compose/watch.go +++ b/cmd/compose/watch.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/compose/v2/cmd/formatter" "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/internal/locker" @@ -31,8 +32,7 @@ import ( type watchOptions struct { *ProjectOptions - quiet bool - noUp bool + noUp bool } func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { @@ -57,13 +57,13 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) ValidArgsFunction: completeServiceNames(dockerCli, p), } - cmd.Flags().BoolVar(&watchOpts.quiet, "quiet", false, "hide build output") + cmd.Flags().BoolVar(&buildOpts.quiet, "quiet", false, "hide build output") cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching") return cmd } func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error { - project, err := watchOpts.ToProject(dockerCli, nil) + project, _, err := watchOpts.ToProject(ctx, dockerCli, nil) if err != nil { return err } @@ -101,7 +101,7 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w Recreate: api.RecreateDiverged, RecreateDependencies: api.RecreateNever, Inherit: true, - QuietPull: watchOpts.quiet, + QuietPull: buildOpts.quiet, }, Start: api.StartOptions{ Project: project, @@ -114,7 +114,10 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w return err } } + + consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false) return backend.Watch(ctx, project, services, api.WatchOptions{ - Build: build, + Build: &build, + LogTo: consumer, }) } diff --git a/cmd/formatter/logs.go b/cmd/formatter/logs.go index c6b8baa320..465aa229a9 100644 --- a/cmd/formatter/logs.go +++ b/cmd/formatter/logs.go @@ -62,7 +62,11 @@ func (l *logConsumer) Register(name string) { func (l *logConsumer) register(name string) *presenter { cf := monochrome if l.color { - cf = nextColor() + if name == api.WatchLogger { + cf = makeColorFunc("92") + } else { + cf = nextColor() + } } p := &presenter{ colors: cf, @@ -138,5 +142,9 @@ type presenter struct { } func (p *presenter) setPrefix(width int) { + if p.name == api.WatchLogger { + p.prefix = p.colors(strings.Repeat(" ", width) + " ⦿ ") + return + } p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name)) } diff --git a/cmd/main.go b/cmd/main.go index d8c011e4d0..038d07cf56 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,6 +25,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/cmd/cmdtrace" "github.com/docker/docker/client" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/docker/compose/v2/cmd/compatibility" @@ -37,7 +38,7 @@ func pluginMain() { plugin.Run(func(dockerCli command.Cli) *cobra.Command { backend := compose.NewComposeService(dockerCli) cmd := commands.RootCommand(dockerCli, backend) - originalPreRun := cmd.PersistentPreRunE + originalPreRunE := cmd.PersistentPreRunE cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { // initialize the dockerCli instance if err := plugin.PersistentPreRunE(cmd, args); err != nil { @@ -46,12 +47,12 @@ func pluginMain() { // compose-specific initialization dockerCliPostInitialize(dockerCli) - // TODO(milas): add an env var to enable logging from the - // OTel components for debugging purposes - _ = cmdtrace.Setup(cmd, dockerCli, os.Args[1:]) + if err := cmdtrace.Setup(cmd, dockerCli, os.Args[1:]); err != nil { + logrus.Debugf("failed to enable tracing: %v", err) + } - if originalPreRun != nil { - return originalPreRun(cmd, args) + if originalPreRunE != nil { + return originalPreRunE(cmd, args) } return nil } diff --git a/docs/reference/compose.md b/docs/reference/compose.md index 3728a76d48..ce6c214d0d 100644 --- a/docs/reference/compose.md +++ b/docs/reference/compose.md @@ -1,42 +1,42 @@ # docker compose -Define and run multi-container applications with Docker. +Define and run multi-container applications with Docker ### Subcommands -| Name | Description | -|:--------------------------------|:-----------------------------------------------------------------------------------------| -| [`attach`](compose_attach.md) | Attach local standard input, output, and error streams to a service's running container. | -| [`build`](compose_build.md) | Build or rebuild services | -| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format | -| [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem | -| [`create`](compose_create.md) | Creates containers for a service. | -| [`down`](compose_down.md) | Stop and remove containers, networks | -| [`events`](compose_events.md) | Receive real time events from containers. | -| [`exec`](compose_exec.md) | Execute a command in a running container. | -| [`images`](compose_images.md) | List images used by the created containers | -| [`kill`](compose_kill.md) | Force stop service containers. | -| [`logs`](compose_logs.md) | View output from containers | -| [`ls`](compose_ls.md) | List running compose projects | -| [`pause`](compose_pause.md) | Pause services | -| [`port`](compose_port.md) | Print the public port for a port binding. | -| [`ps`](compose_ps.md) | List containers | -| [`pull`](compose_pull.md) | Pull service images | -| [`push`](compose_push.md) | Push service images | -| [`restart`](compose_restart.md) | Restart service containers | -| [`rm`](compose_rm.md) | Removes stopped service containers | -| [`run`](compose_run.md) | Run a one-off command on a service. | -| [`scale`](compose_scale.md) | Scale services | -| [`start`](compose_start.md) | Start services | -| [`stats`](compose_stats.md) | Display a live stream of container(s) resource usage statistics | -| [`stop`](compose_stop.md) | Stop services | -| [`top`](compose_top.md) | Display the running processes | -| [`unpause`](compose_unpause.md) | Unpause services | -| [`up`](compose_up.md) | Create and start containers | -| [`version`](compose_version.md) | Show the Docker Compose version information | -| [`wait`](compose_wait.md) | Block until the first service container stops | -| [`watch`](compose_watch.md) | Watch build context for service and rebuild/refresh containers when files are updated | +| Name | Description | +|:--------------------------------|:----------------------------------------------------------------------------------------| +| [`attach`](compose_attach.md) | Attach local standard input, output, and error streams to a service's running container | +| [`build`](compose_build.md) | Build or rebuild services | +| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format | +| [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem | +| [`create`](compose_create.md) | Creates containers for a service | +| [`down`](compose_down.md) | Stop and remove containers, networks | +| [`events`](compose_events.md) | Receive real time events from containers | +| [`exec`](compose_exec.md) | Execute a command in a running container | +| [`images`](compose_images.md) | List images used by the created containers | +| [`kill`](compose_kill.md) | Force stop service containers | +| [`logs`](compose_logs.md) | View output from containers | +| [`ls`](compose_ls.md) | List running compose projects | +| [`pause`](compose_pause.md) | Pause services | +| [`port`](compose_port.md) | Print the public port for a port binding | +| [`ps`](compose_ps.md) | List containers | +| [`pull`](compose_pull.md) | Pull service images | +| [`push`](compose_push.md) | Push service images | +| [`restart`](compose_restart.md) | Restart service containers | +| [`rm`](compose_rm.md) | Removes stopped service containers | +| [`run`](compose_run.md) | Run a one-off command on a service | +| [`scale`](compose_scale.md) | Scale services | +| [`start`](compose_start.md) | Start services | +| [`stats`](compose_stats.md) | Display a live stream of container(s) resource usage statistics | +| [`stop`](compose_stop.md) | Stop services | +| [`top`](compose_top.md) | Display the running processes | +| [`unpause`](compose_unpause.md) | Unpause services | +| [`up`](compose_up.md) | Create and start containers | +| [`version`](compose_version.md) | Show the Docker Compose version information | +| [`wait`](compose_wait.md) | Block until the first service container stops | +| [`watch`](compose_watch.md) | Watch build context for service and rebuild/refresh containers when files are updated | ### Options @@ -46,7 +46,7 @@ Define and run multi-container applications with Docker. | `--ansi` | `string` | `auto` | Control when to print ANSI control characters ("never"\|"always"\|"auto") | | `--compatibility` | | | Run compose in backward compatibility mode | | `--dry-run` | | | Execute command in dry run mode | -| `--env-file` | `stringArray` | | Specify an alternate environment file. | +| `--env-file` | `stringArray` | | Specify an alternate environment file | | `-f`, `--file` | `stringArray` | | Compose configuration files | | `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited | | `--profile` | `stringArray` | | Specify a profile to enable | diff --git a/docs/reference/compose_alpha_dry-run.md b/docs/reference/compose_alpha_dry-run.md index 9e8350e2a0..7c68d94d66 100644 --- a/docs/reference/compose_alpha_dry-run.md +++ b/docs/reference/compose_alpha_dry-run.md @@ -1,7 +1,7 @@ # docker compose alpha dry-run -Dry run command allows you to test a command without applying changes. +Dry run command allows you to test a command without applying changes diff --git a/docs/reference/compose_alpha_publish.md b/docs/reference/compose_alpha_publish.md index 8424d7fbce..02516d968b 100644 --- a/docs/reference/compose_alpha_publish.md +++ b/docs/reference/compose_alpha_publish.md @@ -9,7 +9,7 @@ Publish compose application |:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------| | `--dry-run` | | | Execute command in dry run mode | | `--oci-version` | `string` | | OCI Image/Artifact specification version (automatically determined by default) | -| `--resolve-image-digests` | | | Pin image tags to digests. | +| `--resolve-image-digests` | | | Pin image tags to digests | diff --git a/docs/reference/compose_alpha_scale.md b/docs/reference/compose_alpha_scale.md index 15536b359c..f783f3335c 100644 --- a/docs/reference/compose_alpha_scale.md +++ b/docs/reference/compose_alpha_scale.md @@ -1,14 +1,14 @@ # docker compose alpha scale -Scale services. +Scale services ### Options | Name | Type | Default | Description | |:------------|:-----|:--------|:--------------------------------| | `--dry-run` | | | Execute command in dry run mode | -| `--no-deps` | | | Don't start linked services | +| `--no-deps` | | | Don't start linked services | diff --git a/docs/reference/compose_attach.md b/docs/reference/compose_attach.md index 405fed98ad..5a03388192 100644 --- a/docs/reference/compose_attach.md +++ b/docs/reference/compose_attach.md @@ -1,7 +1,7 @@ # docker compose attach -Attach local standard input, output, and error streams to a service's running container. +Attach local standard input, output, and error streams to a service's running container ### Options @@ -15,4 +15,3 @@ Attach local standard input, output, and error streams to a service's running co - diff --git a/docs/reference/compose_build.md b/docs/reference/compose_build.md index 46bcedbbe7..a0ceb050c0 100644 --- a/docs/reference/compose_build.md +++ b/docs/reference/compose_build.md @@ -7,16 +7,16 @@ Build or rebuild services | Name | Type | Default | Description | |:----------------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------| -| `--build-arg` | `stringArray` | | Set build-time variables for services. | -| `--builder` | `string` | | Set builder to use. | +| `--build-arg` | `stringArray` | | Set build-time variables for services | +| `--builder` | `string` | | Set builder to use | | `--dry-run` | | | Execute command in dry run mode | | `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. | | `--no-cache` | | | Do not use cache when building the image | -| `--pull` | | | Always attempt to pull a newer version of the image. | -| `--push` | | | Push service images. | +| `--pull` | | | Always attempt to pull a newer version of the image | +| `--push` | | | Push service images | | `-q`, `--quiet` | | | Don't print anything to STDOUT | | `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) | -| `--with-dependencies` | | | Also build dependencies (transitively). | +| `--with-dependencies` | | | Also build dependencies (transitively) | diff --git a/docs/reference/compose_config.md b/docs/reference/compose_config.md index 2639cd735e..fd213b4c38 100644 --- a/docs/reference/compose_config.md +++ b/docs/reference/compose_config.md @@ -13,18 +13,18 @@ Parse, resolve and render compose file in canonical format |:--------------------------|:---------|:--------|:----------------------------------------------------------------------------| | `--dry-run` | | | Execute command in dry run mode | | `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] | -| `--hash` | `string` | | Print the service config hash, one per line. | -| `--images` | | | Print the image names, one per line. | +| `--hash` | `string` | | Print the service config hash, one per line | +| `--images` | | | Print the image names, one per line | | `--no-consistency` | | | Don't check model consistency - warning: may produce invalid Compose output | -| `--no-interpolate` | | | Don't interpolate environment variables. | -| `--no-normalize` | | | Don't normalize compose model. | -| `--no-path-resolution` | | | Don't resolve file paths. | +| `--no-interpolate` | | | Don't interpolate environment variables | +| `--no-normalize` | | | Don't normalize compose model | +| `--no-path-resolution` | | | Don't resolve file paths | | `-o`, `--output` | `string` | | Save to file (default to stdout) | -| `--profiles` | | | Print the profile names, one per line. | -| `-q`, `--quiet` | | | Only validate the configuration, don't print anything. | -| `--resolve-image-digests` | | | Pin image tags to digests. | -| `--services` | | | Print the service names, one per line. | -| `--volumes` | | | Print the volume names, one per line. | +| `--profiles` | | | Print the profile names, one per line | +| `-q`, `--quiet` | | | Only validate the configuration, don't print anything | +| `--resolve-image-digests` | | | Pin image tags to digests | +| `--services` | | | Print the service names, one per line | +| `--volumes` | | | Print the volume names, one per line | diff --git a/docs/reference/compose_cp.md b/docs/reference/compose_cp.md index 9be79443af..a60388e306 100644 --- a/docs/reference/compose_cp.md +++ b/docs/reference/compose_cp.md @@ -10,7 +10,7 @@ Copy files/folders between a service container and the local filesystem | `-a`, `--archive` | | | Archive mode (copy all uid/gid information) | | `--dry-run` | | | Execute command in dry run mode | | `-L`, `--follow-link` | | | Always follow symbol link in SRC_PATH | -| `--index` | `int` | `0` | index of the container if service has multiple replicas | +| `--index` | `int` | `0` | Index of the container if service has multiple replicas | diff --git a/docs/reference/compose_create.md b/docs/reference/compose_create.md index 149ed6fe33..06293625a1 100644 --- a/docs/reference/compose_create.md +++ b/docs/reference/compose_create.md @@ -1,19 +1,20 @@ # docker compose create -Creates containers for a service. +Creates containers for a service ### Options | Name | Type | Default | Description | |:-------------------|:--------------|:---------|:----------------------------------------------------------------------------------------------| -| `--build` | | | Build images before starting containers. | +| `--build` | | | Build images before starting containers | | `--dry-run` | | | Execute command in dry run mode | -| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed. | -| `--no-build` | | | Don't build an image, even if it's policy. | +| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed | +| `--no-build` | | | Don't build an image, even if it's policy | | `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. | | `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never"\|"build") | -| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. | +| `--quiet-pull` | | | Pull without printing progress information | +| `--remove-orphans` | | | Remove containers for services not defined in the Compose file | | `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. | diff --git a/docs/reference/compose_down.md b/docs/reference/compose_down.md index 4012b70ca4..2d7cf57271 100644 --- a/docs/reference/compose_down.md +++ b/docs/reference/compose_down.md @@ -5,13 +5,13 @@ Stop and remove containers, networks ### Options -| Name | Type | Default | Description | -|:-------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------| -| `--dry-run` | | | Execute command in dry run mode | -| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. | -| `--rmi` | `string` | | Remove images used by services. "local" remove only images that don't have a custom tag ("local"\|"all") | -| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds | -| `-v`, `--volumes` | | | Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers. | +| Name | Type | Default | Description | +|:-------------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------------| +| `--dry-run` | | | Execute command in dry run mode | +| `--remove-orphans` | | | Remove containers for services not defined in the Compose file | +| `--rmi` | `string` | | Remove images used by services. "local" remove only images that don't have a custom tag ("local"\|"all") | +| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds | +| `-v`, `--volumes` | | | Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers | diff --git a/docs/reference/compose_events.md b/docs/reference/compose_events.md index 86c492e843..0a97a46bc7 100644 --- a/docs/reference/compose_events.md +++ b/docs/reference/compose_events.md @@ -1,7 +1,7 @@ # docker compose events -Receive real time events from containers. +Receive real time events from containers ### Options @@ -33,4 +33,4 @@ With the `--json` flag, a json object is printed one per line with the format: } ``` -The events that can be received using this can be seen [here](https://docs.docker.com/engine/reference/commandline/system_events/#object-types). +The events that can be received using this can be seen [here](https://docs.docker.com/reference/cli/docker/system/events/#object-types). diff --git a/docs/reference/compose_exec.md b/docs/reference/compose_exec.md index 7c5f7d6a4f..fab8221782 100644 --- a/docs/reference/compose_exec.md +++ b/docs/reference/compose_exec.md @@ -1,20 +1,20 @@ # docker compose exec -Execute a command in a running container. +Execute a command in a running container ### Options | Name | Type | Default | Description | |:------------------|:--------------|:--------|:---------------------------------------------------------------------------------| -| `-d`, `--detach` | | | Detached mode: Run command in the background. | +| `-d`, `--detach` | | | Detached mode: Run command in the background | | `--dry-run` | | | Execute command in dry run mode | | `-e`, `--env` | `stringArray` | | Set environment variables | -| `--index` | `int` | `0` | index of the container if service has multiple replicas | +| `--index` | `int` | `0` | Index of the container if service has multiple replicas | | `-T`, `--no-TTY` | | | Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. | -| `--privileged` | | | Give extended privileges to the process. | -| `-u`, `--user` | `string` | | Run the command as this user. | -| `-w`, `--workdir` | `string` | | Path to workdir directory for this command. | +| `--privileged` | | | Give extended privileges to the process | +| `-u`, `--user` | `string` | | Run the command as this user | +| `-w`, `--workdir` | `string` | | Path to workdir directory for this command | diff --git a/docs/reference/compose_images.md b/docs/reference/compose_images.md index 02a8f57ec4..a29af42f34 100644 --- a/docs/reference/compose_images.md +++ b/docs/reference/compose_images.md @@ -5,11 +5,11 @@ List images used by the created containers ### Options -| Name | Type | Default | Description | -|:----------------|:---------|:--------|:--------------------------------------------| -| `--dry-run` | | | Execute command in dry run mode | -| `--format` | `string` | `table` | Format the output. Values: [table \| json]. | -| `-q`, `--quiet` | | | Only display IDs | +| Name | Type | Default | Description | +|:----------------|:---------|:--------|:-------------------------------------------| +| `--dry-run` | | | Execute command in dry run mode | +| `--format` | `string` | `table` | Format the output. Values: [table \| json] | +| `-q`, `--quiet` | | | Only display IDs | diff --git a/docs/reference/compose_kill.md b/docs/reference/compose_kill.md index 2e79d806e9..a10ce55bef 100644 --- a/docs/reference/compose_kill.md +++ b/docs/reference/compose_kill.md @@ -1,15 +1,15 @@ # docker compose kill -Force stop service containers. +Force stop service containers ### Options -| Name | Type | Default | Description | -|:-------------------|:---------|:----------|:----------------------------------------------------------------| -| `--dry-run` | | | Execute command in dry run mode | -| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. | -| `-s`, `--signal` | `string` | `SIGKILL` | SIGNAL to send to the container. | +| Name | Type | Default | Description | +|:-------------------|:---------|:----------|:---------------------------------------------------------------| +| `--dry-run` | | | Execute command in dry run mode | +| `--remove-orphans` | | | Remove containers for services not defined in the Compose file | +| `-s`, `--signal` | `string` | `SIGKILL` | SIGNAL to send to the container | diff --git a/docs/reference/compose_logs.md b/docs/reference/compose_logs.md index b6c705bab2..15291f71d9 100644 --- a/docs/reference/compose_logs.md +++ b/docs/reference/compose_logs.md @@ -8,13 +8,13 @@ View output from containers | Name | Type | Default | Description | |:---------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------| | `--dry-run` | | | Execute command in dry run mode | -| `-f`, `--follow` | | | Follow log output. | +| `-f`, `--follow` | | | Follow log output | | `--index` | `int` | `0` | index of the container if service has multiple replicas | -| `--no-color` | | | Produce monochrome output. | -| `--no-log-prefix` | | | Don't print prefix in logs. | +| `--no-color` | | | Produce monochrome output | +| `--no-log-prefix` | | | Don't print prefix in logs | | `--since` | `string` | | Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) | -| `-n`, `--tail` | `string` | `all` | Number of lines to show from the end of the logs for each container. | -| `-t`, `--timestamps` | | | Show timestamps. | +| `-n`, `--tail` | `string` | `all` | Number of lines to show from the end of the logs for each container | +| `-t`, `--timestamps` | | | Show timestamps | | `--until` | `string` | | Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) | @@ -22,4 +22,4 @@ View output from containers ## Description -Displays log output from services. \ No newline at end of file +Displays log output from services diff --git a/docs/reference/compose_ls.md b/docs/reference/compose_ls.md index 50a13d96f0..a1148a1675 100644 --- a/docs/reference/compose_ls.md +++ b/docs/reference/compose_ls.md @@ -5,17 +5,17 @@ List running compose projects ### Options -| Name | Type | Default | Description | -|:----------------|:---------|:--------|:--------------------------------------------| -| `-a`, `--all` | | | Show all stopped Compose projects | -| `--dry-run` | | | Execute command in dry run mode | -| `--filter` | `filter` | | Filter output based on conditions provided. | -| `--format` | `string` | `table` | Format the output. Values: [table \| json]. | -| `-q`, `--quiet` | | | Only display IDs. | +| Name | Type | Default | Description | +|:----------------|:---------|:--------|:-------------------------------------------| +| `-a`, `--all` | | | Show all stopped Compose projects | +| `--dry-run` | | | Execute command in dry run mode | +| `--filter` | `filter` | | Filter output based on conditions provided | +| `--format` | `string` | `table` | Format the output. Values: [table \| json] | +| `-q`, `--quiet` | | | Only display IDs | ## Description -Lists running Compose projects. \ No newline at end of file +Lists running Compose projects diff --git a/docs/reference/compose_port.md b/docs/reference/compose_port.md index ffd3d8eb53..5e70b35329 100644 --- a/docs/reference/compose_port.md +++ b/docs/reference/compose_port.md @@ -1,14 +1,14 @@ # docker compose port -Print the public port for a port binding. +Print the public port for a port binding ### Options | Name | Type | Default | Description | |:-------------|:---------|:--------|:--------------------------------------------------------| | `--dry-run` | | | Execute command in dry run mode | -| `--index` | `int` | `0` | index of the container if service has multiple replicas | +| `--index` | `int` | `0` | Index of the container if service has multiple replicas | | `--protocol` | `string` | `tcp` | tcp or udp | @@ -16,4 +16,4 @@ Print the public port for a port binding. ## Description -Prints the public port for a port binding. \ No newline at end of file +Prints the public port for a port binding diff --git a/docs/reference/compose_ps.md b/docs/reference/compose_ps.md index 81ef255098..f0c1a25762 100644 --- a/docs/reference/compose_ps.md +++ b/docs/reference/compose_ps.md @@ -9,7 +9,7 @@ List containers |:----------------------|:--------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-a`, `--all` | | | Show all stopped containers (including those created by the run command) | | `--dry-run` | | | Execute command in dry run mode | -| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). | +| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status) | | [`--format`](#format) | `string` | `table` | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | | `--no-trunc` | | | Don't truncate output | | `--orphans` | | | Include orphaned services (not declared by project) | diff --git a/docs/reference/compose_pull.md b/docs/reference/compose_pull.md index 03be4de05e..2c29052fd8 100644 --- a/docs/reference/compose_pull.md +++ b/docs/reference/compose_pull.md @@ -5,22 +5,21 @@ Pull service images ### Options -| Name | Type | Default | Description | -|:-------------------------|:---------|:--------|:--------------------------------------------------------| -| `--dry-run` | | | Execute command in dry run mode | -| `--ignore-buildable` | | | Ignore images that can be built. | -| `--ignore-pull-failures` | | | Pull what it can and ignores images with pull failures. | -| `--include-deps` | | | Also pull services declared as dependencies. | -| `--policy` | `string` | | Apply pull policy ("missing"\|"always"). | -| `-q`, `--quiet` | | | Pull without printing progress information. | +| Name | Type | Default | Description | +|:-------------------------|:---------|:--------|:-------------------------------------------------------| +| `--dry-run` | | | Execute command in dry run mode | +| `--ignore-buildable` | | | Ignore images that can be built | +| `--ignore-pull-failures` | | | Pull what it can and ignores images with pull failures | +| `--include-deps` | | | Also pull services declared as dependencies | +| `--policy` | `string` | | Apply pull policy ("missing"\|"always") | +| `-q`, `--quiet` | | | Pull without printing progress information | ## Description -Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on -those images. +Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on those images ## Examples diff --git a/docs/reference/compose_restart.md b/docs/reference/compose_restart.md index 1e7bdf3c8c..d77b461a85 100644 --- a/docs/reference/compose_restart.md +++ b/docs/reference/compose_restart.md @@ -8,7 +8,7 @@ Restart service containers | Name | Type | Default | Description | |:------------------|:------|:--------|:--------------------------------------| | `--dry-run` | | | Execute command in dry run mode | -| `--no-deps` | | | Don't restart dependent services. | +| `--no-deps` | | | Don't restart dependent services | | `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds | diff --git a/docs/reference/compose_run.md b/docs/reference/compose_run.md index 033ac5e78d..185b4ad073 100644 --- a/docs/reference/compose_run.md +++ b/docs/reference/compose_run.md @@ -1,33 +1,33 @@ # docker compose run -Run a one-off command on a service. +Run a one-off command on a service ### Options -| Name | Type | Default | Description | -|:------------------------|:--------------|:--------|:----------------------------------------------------------------------------------| -| `--build` | | | Build image before starting container. | -| `--cap-add` | `list` | | Add Linux capabilities | -| `--cap-drop` | `list` | | Drop Linux capabilities | -| `-d`, `--detach` | | | Run container in background and print container ID | -| `--dry-run` | | | Execute command in dry run mode | -| `--entrypoint` | `string` | | Override the entrypoint of the image | -| `-e`, `--env` | `stringArray` | | Set environment variables | -| `-i`, `--interactive` | | | Keep STDIN open even if not attached. | -| `-l`, `--label` | `stringArray` | | Add or override a label | -| `--name` | `string` | | Assign a name to the container | -| `-T`, `--no-TTY` | | | Disable pseudo-TTY allocation (default: auto-detected). | -| `--no-deps` | | | Don't start linked services. | -| `-p`, `--publish` | `stringArray` | | Publish a container's port(s) to the host. | -| `--quiet-pull` | | | Pull without printing progress information. | -| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. | -| `--rm` | | | Automatically remove the container when it exits | -| `-P`, `--service-ports` | | | Run command with all service's ports enabled and mapped to the host. | -| `--use-aliases` | | | Use the service's network useAliases in the network(s) the container connects to. | -| `-u`, `--user` | `string` | | Run as specified username or uid | -| `-v`, `--volume` | `stringArray` | | Bind mount a volume. | -| `-w`, `--workdir` | `string` | | Working directory inside the container | +| Name | Type | Default | Description | +|:------------------------|:--------------|:--------|:---------------------------------------------------------------------------------| +| `--build` | | | Build image before starting container | +| `--cap-add` | `list` | | Add Linux capabilities | +| `--cap-drop` | `list` | | Drop Linux capabilities | +| `-d`, `--detach` | | | Run container in background and print container ID | +| `--dry-run` | | | Execute command in dry run mode | +| `--entrypoint` | `string` | | Override the entrypoint of the image | +| `-e`, `--env` | `stringArray` | | Set environment variables | +| `-i`, `--interactive` | | | Keep STDIN open even if not attached | +| `-l`, `--label` | `stringArray` | | Add or override a label | +| `--name` | `string` | | Assign a name to the container | +| `-T`, `--no-TTY` | | | Disable pseudo-TTY allocation (default: auto-detected) | +| `--no-deps` | | | Don't start linked services | +| `-p`, `--publish` | `stringArray` | | Publish a container's port(s) to the host | +| `--quiet-pull` | | | Pull without printing progress information | +| `--remove-orphans` | | | Remove containers for services not defined in the Compose file | +| `--rm` | | | Automatically remove the container when it exits | +| `-P`, `--service-ports` | | | Run command with all service's ports enabled and mapped to the host | +| `--use-aliases` | | | Use the service's network useAliases in the network(s) the container connects to | +| `-u`, `--user` | `string` | | Run as specified username or uid | +| `-v`, `--volume` | `stringArray` | | Bind mount a volume | +| `-w`, `--workdir` | `string` | | Working directory inside the container | diff --git a/docs/reference/compose_scale.md b/docs/reference/compose_scale.md index 5cf5830e28..e30508328a 100644 --- a/docs/reference/compose_scale.md +++ b/docs/reference/compose_scale.md @@ -8,7 +8,7 @@ Scale services | Name | Type | Default | Description | |:------------|:-----|:--------|:--------------------------------| | `--dry-run` | | | Execute command in dry run mode | -| `--no-deps` | | | Don't start linked services. | +| `--no-deps` | | | Don't start linked services | diff --git a/docs/reference/compose_start.md b/docs/reference/compose_start.md index b525347f7e..4ea26e9b59 100644 --- a/docs/reference/compose_start.md +++ b/docs/reference/compose_start.md @@ -14,4 +14,4 @@ Start services ## Description -Starts existing containers for a service. +Starts existing containers for a service diff --git a/docs/reference/compose_top.md b/docs/reference/compose_top.md index 9aff0ce71f..a23373832a 100644 --- a/docs/reference/compose_top.md +++ b/docs/reference/compose_top.md @@ -14,7 +14,7 @@ Display the running processes ## Description -Displays the running processes. +Displays the running processes ## Examples diff --git a/docs/reference/compose_unpause.md b/docs/reference/compose_unpause.md index 0df10a9924..9d810e4710 100644 --- a/docs/reference/compose_unpause.md +++ b/docs/reference/compose_unpause.md @@ -14,4 +14,4 @@ Unpause services ## Description -Unpauses paused containers of a service. +Unpauses paused containers of a service diff --git a/docs/reference/compose_up.md b/docs/reference/compose_up.md index 49fc17225a..a34766c484 100644 --- a/docs/reference/compose_up.md +++ b/docs/reference/compose_up.md @@ -5,33 +5,34 @@ Create and start containers ### Options -| Name | Type | Default | Description | -|:-----------------------------|:--------------|:---------|:---------------------------------------------------------------------------------------------------------| -| `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d | -| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. | -| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. | -| `--attach-dependencies` | | | Automatically attach to log output of dependent services. | -| `--build` | | | Build images before starting containers. | -| `-d`, `--detach` | | | Detached mode: Run containers in the background | -| `--dry-run` | | | Execute command in dry run mode | -| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit | -| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed. | -| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services. | -| `--no-build` | | | Don't build an image, even if it's policy. | -| `--no-color` | | | Produce monochrome output. | -| `--no-deps` | | | Don't start linked services. | -| `--no-log-prefix` | | | Don't print prefix in logs. | -| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. | -| `--no-start` | | | Don't start the services after creating them. | -| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") | -| `--quiet-pull` | | | Pull without printing progress information. | -| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. | -| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers. | -| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. | -| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running. | -| `--timestamps` | | | Show timestamps. | -| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. | -| `--wait-timeout` | `int` | `0` | Maximum duration to wait for the project to be running\|healthy. | +| Name | Type | Default | Description | +|:-----------------------------|:--------------|:---------|:--------------------------------------------------------------------------------------------------------| +| `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d | +| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. | +| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. | +| `--attach-dependencies` | | | Automatically attach to log output of dependent services | +| `--build` | | | Build images before starting containers | +| `-d`, `--detach` | | | Detached mode: Run containers in the background | +| `--dry-run` | | | Execute command in dry run mode | +| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit | +| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed | +| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services | +| `--no-build` | | | Don't build an image, even if it's policy | +| `--no-color` | | | Produce monochrome output | +| `--no-deps` | | | Don't start linked services | +| `--no-log-prefix` | | | Don't print prefix in logs | +| `--no-recreate` | | | If containers already exist, don't recreate them. Incompatible with --force-recreate. | +| `--no-start` | | | Don't start the services after creating them | +| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") | +| `--quiet-pull` | | | Pull without printing progress information | +| `--remove-orphans` | | | Remove containers for services not defined in the Compose file | +| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers | +| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. | +| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running | +| `--timestamps` | | | Show timestamps | +| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. | +| `--wait-timeout` | `int` | `0` | Maximum duration to wait for the project to be running\|healthy | +| `-w`, `--watch` | | | Watch source code and rebuild/refresh containers when files are updated. | diff --git a/docs/reference/compose_version.md b/docs/reference/compose_version.md index 66081d65ec..9284d8e935 100644 --- a/docs/reference/compose_version.md +++ b/docs/reference/compose_version.md @@ -9,7 +9,7 @@ Show the Docker Compose version information |:-----------------|:---------|:--------|:---------------------------------------------------------------| | `--dry-run` | | | Execute command in dry run mode | | `-f`, `--format` | `string` | | Format the output. Values: [pretty \| json]. (Default: pretty) | -| `--short` | | | Shows only Compose's version number. | +| `--short` | | | Shows only Compose's version number | diff --git a/docs/reference/docker_compose.yaml b/docs/reference/docker_compose.yaml index 83f555b6c8..acdd391261 100644 --- a/docs/reference/docker_compose.yaml +++ b/docs/reference/docker_compose.yaml @@ -242,7 +242,7 @@ options: - option: env-file value_type: stringArray default_value: '[]' - description: Specify an alternate environment file. + description: Specify an alternate environment file deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_alpha_publish.yaml b/docs/reference/docker_compose_alpha_publish.yaml index f2237f2ae4..38868104a5 100644 --- a/docs/reference/docker_compose_alpha_publish.yaml +++ b/docs/reference/docker_compose_alpha_publish.yaml @@ -18,7 +18,7 @@ options: - option: resolve-image-digests value_type: bool default_value: "false" - description: Pin image tags to digests. + description: Pin image tags to digests deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_attach.yaml b/docs/reference/docker_compose_attach.yaml index 14c584f411..8fd6957ca1 100644 --- a/docs/reference/docker_compose_attach.yaml +++ b/docs/reference/docker_compose_attach.yaml @@ -1,8 +1,8 @@ command: docker compose attach short: | - Attach local standard input, output, and error streams to a service's running container. + Attach local standard input, output, and error streams to a service's running container long: | - Attach local standard input, output, and error streams to a service's running container. + Attach local standard input, output, and error streams to a service's running container usage: docker compose attach [OPTIONS] SERVICE pname: docker compose plink: docker_compose.yaml diff --git a/docs/reference/docker_compose_build.yaml b/docs/reference/docker_compose_build.yaml index d6085d0d15..34175696fb 100644 --- a/docs/reference/docker_compose_build.yaml +++ b/docs/reference/docker_compose_build.yaml @@ -17,7 +17,7 @@ options: - option: build-arg value_type: stringArray default_value: '[]' - description: Set build-time variables for services. + description: Set build-time variables for services deprecated: false hidden: false experimental: false @@ -26,7 +26,7 @@ options: swarm: false - option: builder value_type: string - description: Set builder to use. + description: Set builder to use deprecated: false hidden: false experimental: false @@ -109,7 +109,7 @@ options: - option: pull value_type: bool default_value: "false" - description: Always attempt to pull a newer version of the image. + description: Always attempt to pull a newer version of the image deprecated: false hidden: false experimental: false @@ -119,7 +119,7 @@ options: - option: push value_type: bool default_value: "false" - description: Push service images. + description: Push service images deprecated: false hidden: false experimental: false @@ -150,7 +150,7 @@ options: - option: with-dependencies value_type: bool default_value: "false" - description: Also build dependencies (transitively). + description: Also build dependencies (transitively) deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_config.yaml b/docs/reference/docker_compose_config.yaml index d180c8d143..ea7669b0d9 100644 --- a/docs/reference/docker_compose_config.yaml +++ b/docs/reference/docker_compose_config.yaml @@ -21,7 +21,7 @@ options: swarm: false - option: hash value_type: string - description: Print the service config hash, one per line. + description: Print the service config hash, one per line deprecated: false hidden: false experimental: false @@ -31,7 +31,7 @@ options: - option: images value_type: bool default_value: "false" - description: Print the image names, one per line. + description: Print the image names, one per line deprecated: false hidden: false experimental: false @@ -52,7 +52,7 @@ options: - option: no-interpolate value_type: bool default_value: "false" - description: Don't interpolate environment variables. + description: Don't interpolate environment variables deprecated: false hidden: false experimental: false @@ -62,7 +62,7 @@ options: - option: no-normalize value_type: bool default_value: "false" - description: Don't normalize compose model. + description: Don't normalize compose model deprecated: false hidden: false experimental: false @@ -72,7 +72,7 @@ options: - option: no-path-resolution value_type: bool default_value: "false" - description: Don't resolve file paths. + description: Don't resolve file paths deprecated: false hidden: false experimental: false @@ -92,7 +92,7 @@ options: - option: profiles value_type: bool default_value: "false" - description: Print the profile names, one per line. + description: Print the profile names, one per line deprecated: false hidden: false experimental: false @@ -103,7 +103,7 @@ options: shorthand: q value_type: bool default_value: "false" - description: Only validate the configuration, don't print anything. + description: Only validate the configuration, don't print anything deprecated: false hidden: false experimental: false @@ -113,7 +113,7 @@ options: - option: resolve-image-digests value_type: bool default_value: "false" - description: Pin image tags to digests. + description: Pin image tags to digests deprecated: false hidden: false experimental: false @@ -123,7 +123,7 @@ options: - option: services value_type: bool default_value: "false" - description: Print the service names, one per line. + description: Print the service names, one per line deprecated: false hidden: false experimental: false @@ -133,7 +133,7 @@ options: - option: volumes value_type: bool default_value: "false" - description: Print the volume names, one per line. + description: Print the volume names, one per line deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_cp.yaml b/docs/reference/docker_compose_cp.yaml index 4551c13c64..8ff3cf37e0 100644 --- a/docs/reference/docker_compose_cp.yaml +++ b/docs/reference/docker_compose_cp.yaml @@ -10,7 +10,7 @@ options: - option: all value_type: bool default_value: "false" - description: copy to all the containers of the service. + description: Copy to all the containers of the service deprecated: true hidden: true experimental: false @@ -42,7 +42,7 @@ options: - option: index value_type: int default_value: "0" - description: index of the container if service has multiple replicas + description: Index of the container if service has multiple replicas deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_create.yaml b/docs/reference/docker_compose_create.yaml index 05a4a51437..a07e1c88cc 100644 --- a/docs/reference/docker_compose_create.yaml +++ b/docs/reference/docker_compose_create.yaml @@ -1,6 +1,6 @@ command: docker compose create -short: Creates containers for a service. -long: Creates containers for a service. +short: Creates containers for a service +long: Creates containers for a service usage: docker compose create [OPTIONS] [SERVICE...] pname: docker compose plink: docker_compose.yaml @@ -8,7 +8,7 @@ options: - option: build value_type: bool default_value: "false" - description: Build images before starting containers. + description: Build images before starting containers deprecated: false hidden: false experimental: false @@ -19,7 +19,7 @@ options: value_type: bool default_value: "false" description: | - Recreate containers even if their configuration and image haven't changed. + Recreate containers even if their configuration and image haven't changed deprecated: false hidden: false experimental: false @@ -29,7 +29,7 @@ options: - option: no-build value_type: bool default_value: "false" - description: Don't build an image, even if it's policy. + description: Don't build an image, even if it's policy deprecated: false hidden: false experimental: false @@ -57,10 +57,20 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: quiet-pull + value_type: bool + default_value: "false" + description: Pull without printing progress information + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: remove-orphans value_type: bool default_value: "false" - description: Remove containers for services not defined in the Compose file. + description: Remove containers for services not defined in the Compose file deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_down.yaml b/docs/reference/docker_compose_down.yaml index 7964dd49c3..77bf526289 100644 --- a/docs/reference/docker_compose_down.yaml +++ b/docs/reference/docker_compose_down.yaml @@ -21,7 +21,7 @@ options: - option: remove-orphans value_type: bool default_value: "false" - description: Remove containers for services not defined in the Compose file. + description: Remove containers for services not defined in the Compose file deprecated: false hidden: false experimental: false @@ -54,7 +54,7 @@ options: value_type: bool default_value: "false" description: | - Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers. + Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_events.yaml b/docs/reference/docker_compose_events.yaml index cd51372f72..fe6d4216ce 100644 --- a/docs/reference/docker_compose_events.yaml +++ b/docs/reference/docker_compose_events.yaml @@ -1,5 +1,5 @@ command: docker compose events -short: Receive real time events from containers. +short: Receive real time events from containers long: |- Stream container events for every container in the project. @@ -19,7 +19,7 @@ long: |- } ``` - The events that can be received using this can be seen [here](/engine/reference/commandline/system_events/#object-types). + The events that can be received using this can be seen [here](/reference/cli/docker/system/events/#object-types). usage: docker compose events [OPTIONS] [SERVICE...] pname: docker compose plink: docker_compose.yaml diff --git a/docs/reference/docker_compose_exec.yaml b/docs/reference/docker_compose_exec.yaml index ce6faeda6d..b2a1cf2068 100644 --- a/docs/reference/docker_compose_exec.yaml +++ b/docs/reference/docker_compose_exec.yaml @@ -1,5 +1,5 @@ command: docker compose exec -short: Execute a command in a running container. +short: Execute a command in a running container long: |- This is the equivalent of `docker exec` targeting a Compose service. @@ -13,7 +13,7 @@ options: shorthand: d value_type: bool default_value: "false" - description: 'Detached mode: Run command in the background.' + description: 'Detached mode: Run command in the background' deprecated: false hidden: false experimental: false @@ -34,7 +34,7 @@ options: - option: index value_type: int default_value: "0" - description: index of the container if service has multiple replicas + description: Index of the container if service has multiple replicas deprecated: false hidden: false experimental: false @@ -45,7 +45,7 @@ options: shorthand: i value_type: bool default_value: "true" - description: Keep STDIN open even if not attached. + description: Keep STDIN open even if not attached deprecated: false hidden: true experimental: false @@ -67,7 +67,7 @@ options: - option: privileged value_type: bool default_value: "false" - description: Give extended privileges to the process. + description: Give extended privileges to the process deprecated: false hidden: false experimental: false @@ -78,7 +78,7 @@ options: shorthand: t value_type: bool default_value: "true" - description: Allocate a pseudo-TTY. + description: Allocate a pseudo-TTY deprecated: false hidden: true experimental: false @@ -88,7 +88,7 @@ options: - option: user shorthand: u value_type: string - description: Run the command as this user. + description: Run the command as this user deprecated: false hidden: false experimental: false @@ -98,7 +98,7 @@ options: - option: workdir shorthand: w value_type: string - description: Path to workdir directory for this command. + description: Path to workdir directory for this command deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_images.yaml b/docs/reference/docker_compose_images.yaml index 4719590af5..33187df42d 100644 --- a/docs/reference/docker_compose_images.yaml +++ b/docs/reference/docker_compose_images.yaml @@ -8,7 +8,7 @@ options: - option: format value_type: string default_value: table - description: 'Format the output. Values: [table | json].' + description: 'Format the output. Values: [table | json]' deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_kill.yaml b/docs/reference/docker_compose_kill.yaml index 2134227641..b6d5334827 100644 --- a/docs/reference/docker_compose_kill.yaml +++ b/docs/reference/docker_compose_kill.yaml @@ -1,5 +1,5 @@ command: docker compose kill -short: Force stop service containers. +short: Force stop service containers long: |- Forces running containers to stop by sending a `SIGKILL` signal. Optionally the signal can be passed, for example: @@ -13,7 +13,7 @@ options: - option: remove-orphans value_type: bool default_value: "false" - description: Remove containers for services not defined in the Compose file. + description: Remove containers for services not defined in the Compose file deprecated: false hidden: false experimental: false @@ -24,7 +24,7 @@ options: shorthand: s value_type: string default_value: SIGKILL - description: SIGNAL to send to the container. + description: SIGNAL to send to the container deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_logs.yaml b/docs/reference/docker_compose_logs.yaml index fe6bbdb424..92d94dd108 100644 --- a/docs/reference/docker_compose_logs.yaml +++ b/docs/reference/docker_compose_logs.yaml @@ -1,6 +1,6 @@ command: docker compose logs short: View output from containers -long: Displays log output from services. +long: Displays log output from services usage: docker compose logs [OPTIONS] [SERVICE...] pname: docker compose plink: docker_compose.yaml @@ -9,7 +9,7 @@ options: shorthand: f value_type: bool default_value: "false" - description: Follow log output. + description: Follow log output deprecated: false hidden: false experimental: false @@ -29,7 +29,7 @@ options: - option: no-color value_type: bool default_value: "false" - description: Produce monochrome output. + description: Produce monochrome output deprecated: false hidden: false experimental: false @@ -39,7 +39,7 @@ options: - option: no-log-prefix value_type: bool default_value: "false" - description: Don't print prefix in logs. + description: Don't print prefix in logs deprecated: false hidden: false experimental: false @@ -61,7 +61,7 @@ options: value_type: string default_value: all description: | - Number of lines to show from the end of the logs for each container. + Number of lines to show from the end of the logs for each container deprecated: false hidden: false experimental: false @@ -72,7 +72,7 @@ options: shorthand: t value_type: bool default_value: "false" - description: Show timestamps. + description: Show timestamps deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_ls.yaml b/docs/reference/docker_compose_ls.yaml index c4b6d5f7c1..a2efac2a88 100644 --- a/docs/reference/docker_compose_ls.yaml +++ b/docs/reference/docker_compose_ls.yaml @@ -1,6 +1,6 @@ command: docker compose ls short: List running compose projects -long: Lists running Compose projects. +long: Lists running Compose projects usage: docker compose ls [OPTIONS] pname: docker compose plink: docker_compose.yaml @@ -18,7 +18,7 @@ options: swarm: false - option: filter value_type: filter - description: Filter output based on conditions provided. + description: Filter output based on conditions provided deprecated: false hidden: false experimental: false @@ -28,7 +28,7 @@ options: - option: format value_type: string default_value: table - description: 'Format the output. Values: [table | json].' + description: 'Format the output. Values: [table | json]' deprecated: false hidden: false experimental: false @@ -39,7 +39,7 @@ options: shorthand: q value_type: bool default_value: "false" - description: Only display IDs. + description: Only display IDs deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_port.yaml b/docs/reference/docker_compose_port.yaml index 7c8dee90ce..8a07f31ea5 100644 --- a/docs/reference/docker_compose_port.yaml +++ b/docs/reference/docker_compose_port.yaml @@ -1,6 +1,6 @@ command: docker compose port -short: Print the public port for a port binding. -long: Prints the public port for a port binding. +short: Print the public port for a port binding +long: Prints the public port for a port binding usage: docker compose port [OPTIONS] SERVICE PRIVATE_PORT pname: docker compose plink: docker_compose.yaml @@ -8,7 +8,7 @@ options: - option: index value_type: int default_value: "0" - description: index of the container if service has multiple replicas + description: Index of the container if service has multiple replicas deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_ps.yaml b/docs/reference/docker_compose_ps.yaml index 78842a9990..d037027569 100644 --- a/docs/reference/docker_compose_ps.yaml +++ b/docs/reference/docker_compose_ps.yaml @@ -35,7 +35,7 @@ options: swarm: false - option: filter value_type: string - description: 'Filter services by a property (supported filters: status).' + description: 'Filter services by a property (supported filters: status)' details_url: '#filter' deprecated: false hidden: false diff --git a/docs/reference/docker_compose_pull.yaml b/docs/reference/docker_compose_pull.yaml index 46a4711624..5b1316df13 100644 --- a/docs/reference/docker_compose_pull.yaml +++ b/docs/reference/docker_compose_pull.yaml @@ -1,8 +1,7 @@ command: docker compose pull short: Pull service images -long: |- - Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on - those images. +long: | + Pulls an image associated with a service defined in a `compose.yaml` file, but does not start containers based on those images usage: docker compose pull [OPTIONS] [SERVICE...] pname: docker compose plink: docker_compose.yaml @@ -10,7 +9,7 @@ options: - option: ignore-buildable value_type: bool default_value: "false" - description: Ignore images that can be built. + description: Ignore images that can be built deprecated: false hidden: false experimental: false @@ -20,7 +19,7 @@ options: - option: ignore-pull-failures value_type: bool default_value: "false" - description: Pull what it can and ignores images with pull failures. + description: Pull what it can and ignores images with pull failures deprecated: false hidden: false experimental: false @@ -30,7 +29,7 @@ options: - option: include-deps value_type: bool default_value: "false" - description: Also pull services declared as dependencies. + description: Also pull services declared as dependencies deprecated: false hidden: false experimental: false @@ -40,7 +39,7 @@ options: - option: no-parallel value_type: bool default_value: "true" - description: DEPRECATED disable parallel pulling. + description: DEPRECATED disable parallel pulling deprecated: false hidden: true experimental: false @@ -50,7 +49,7 @@ options: - option: parallel value_type: bool default_value: "true" - description: DEPRECATED pull multiple images in parallel. + description: DEPRECATED pull multiple images in parallel deprecated: false hidden: true experimental: false @@ -59,7 +58,7 @@ options: swarm: false - option: policy value_type: string - description: Apply pull policy ("missing"|"always"). + description: Apply pull policy ("missing"|"always") deprecated: false hidden: false experimental: false @@ -70,7 +69,7 @@ options: shorthand: q value_type: bool default_value: "false" - description: Pull without printing progress information. + description: Pull without printing progress information deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_restart.yaml b/docs/reference/docker_compose_restart.yaml index 3126eb005a..3b2a4bddd1 100644 --- a/docs/reference/docker_compose_restart.yaml +++ b/docs/reference/docker_compose_restart.yaml @@ -18,7 +18,7 @@ options: - option: no-deps value_type: bool default_value: "false" - description: Don't restart dependent services. + description: Don't restart dependent services deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_run.yaml b/docs/reference/docker_compose_run.yaml index 12abafc3b6..0584bdd070 100644 --- a/docs/reference/docker_compose_run.yaml +++ b/docs/reference/docker_compose_run.yaml @@ -1,5 +1,5 @@ command: docker compose run -short: Run a one-off command on a service. +short: Run a one-off command on a service long: |- Runs a one-time command against a service. @@ -61,7 +61,7 @@ options: - option: build value_type: bool default_value: "false" - description: Build image before starting container. + description: Build image before starting container deprecated: false hidden: false experimental: false @@ -121,7 +121,7 @@ options: shorthand: i value_type: bool default_value: "true" - description: Keep STDIN open even if not attached. + description: Keep STDIN open even if not attached deprecated: false hidden: false experimental: false @@ -152,7 +152,7 @@ options: shorthand: T value_type: bool default_value: "true" - description: 'Disable pseudo-TTY allocation (default: auto-detected).' + description: 'Disable pseudo-TTY allocation (default: auto-detected)' deprecated: false hidden: false experimental: false @@ -162,7 +162,7 @@ options: - option: no-deps value_type: bool default_value: "false" - description: Don't start linked services. + description: Don't start linked services deprecated: false hidden: false experimental: false @@ -173,7 +173,7 @@ options: shorthand: p value_type: stringArray default_value: '[]' - description: Publish a container's port(s) to the host. + description: Publish a container's port(s) to the host deprecated: false hidden: false experimental: false @@ -183,7 +183,7 @@ options: - option: quiet-pull value_type: bool default_value: "false" - description: Pull without printing progress information. + description: Pull without printing progress information deprecated: false hidden: false experimental: false @@ -193,7 +193,7 @@ options: - option: remove-orphans value_type: bool default_value: "false" - description: Remove containers for services not defined in the Compose file. + description: Remove containers for services not defined in the Compose file deprecated: false hidden: false experimental: false @@ -215,7 +215,7 @@ options: value_type: bool default_value: "false" description: | - Run command with all service's ports enabled and mapped to the host. + Run command with all service's ports enabled and mapped to the host deprecated: false hidden: false experimental: false @@ -226,7 +226,7 @@ options: shorthand: t value_type: bool default_value: "true" - description: Allocate a pseudo-TTY. + description: Allocate a pseudo-TTY deprecated: false hidden: true experimental: false @@ -237,7 +237,7 @@ options: value_type: bool default_value: "false" description: | - Use the service's network useAliases in the network(s) the container connects to. + Use the service's network useAliases in the network(s) the container connects to deprecated: false hidden: false experimental: false @@ -258,7 +258,7 @@ options: shorthand: v value_type: stringArray default_value: '[]' - description: Bind mount a volume. + description: Bind mount a volume deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_scale.yaml b/docs/reference/docker_compose_scale.yaml index 9391441f26..f840a51b4e 100644 --- a/docs/reference/docker_compose_scale.yaml +++ b/docs/reference/docker_compose_scale.yaml @@ -8,7 +8,7 @@ options: - option: no-deps value_type: bool default_value: "false" - description: Don't start linked services. + description: Don't start linked services deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_start.yaml b/docs/reference/docker_compose_start.yaml index 9a7fa379be..902b688d3e 100644 --- a/docs/reference/docker_compose_start.yaml +++ b/docs/reference/docker_compose_start.yaml @@ -1,6 +1,6 @@ command: docker compose start short: Start services -long: Starts existing containers for a service. +long: Starts existing containers for a service usage: docker compose start [SERVICE...] pname: docker compose plink: docker_compose.yaml diff --git a/docs/reference/docker_compose_top.yaml b/docs/reference/docker_compose_top.yaml index 7d87f7ccab..17cdf7e381 100644 --- a/docs/reference/docker_compose_top.yaml +++ b/docs/reference/docker_compose_top.yaml @@ -1,6 +1,6 @@ command: docker compose top short: Display the running processes -long: Displays the running processes. +long: Displays the running processes usage: docker compose top [SERVICES...] pname: docker compose plink: docker_compose.yaml diff --git a/docs/reference/docker_compose_unpause.yaml b/docs/reference/docker_compose_unpause.yaml index 2679f53f69..e2047720b8 100644 --- a/docs/reference/docker_compose_unpause.yaml +++ b/docs/reference/docker_compose_unpause.yaml @@ -1,6 +1,6 @@ command: docker compose unpause short: Unpause services -long: Unpauses paused containers of a service. +long: Unpauses paused containers of a service usage: docker compose unpause [SERVICE...] pname: docker compose plink: docker_compose.yaml diff --git a/docs/reference/docker_compose_up.yaml b/docs/reference/docker_compose_up.yaml index 2ee4d199c6..ec269c8b85 100644 --- a/docs/reference/docker_compose_up.yaml +++ b/docs/reference/docker_compose_up.yaml @@ -59,7 +59,7 @@ options: - option: attach-dependencies value_type: bool default_value: "false" - description: Automatically attach to log output of dependent services. + description: Automatically attach to log output of dependent services deprecated: false hidden: false experimental: false @@ -69,7 +69,7 @@ options: - option: build value_type: bool default_value: "false" - description: Build images before starting containers. + description: Build images before starting containers deprecated: false hidden: false experimental: false @@ -101,7 +101,7 @@ options: value_type: bool default_value: "false" description: | - Recreate containers even if their configuration and image haven't changed. + Recreate containers even if their configuration and image haven't changed deprecated: false hidden: false experimental: false @@ -111,7 +111,7 @@ options: - option: no-attach value_type: stringArray default_value: '[]' - description: Do not attach (stream logs) to the specified services. + description: Do not attach (stream logs) to the specified services deprecated: false hidden: false experimental: false @@ -121,7 +121,7 @@ options: - option: no-build value_type: bool default_value: "false" - description: Don't build an image, even if it's policy. + description: Don't build an image, even if it's policy deprecated: false hidden: false experimental: false @@ -131,7 +131,7 @@ options: - option: no-color value_type: bool default_value: "false" - description: Produce monochrome output. + description: Produce monochrome output deprecated: false hidden: false experimental: false @@ -141,7 +141,7 @@ options: - option: no-deps value_type: bool default_value: "false" - description: Don't start linked services. + description: Don't start linked services deprecated: false hidden: false experimental: false @@ -151,7 +151,7 @@ options: - option: no-log-prefix value_type: bool default_value: "false" - description: Don't print prefix in logs. + description: Don't print prefix in logs deprecated: false hidden: false experimental: false @@ -172,7 +172,7 @@ options: - option: no-start value_type: bool default_value: "false" - description: Don't start the services after creating them. + description: Don't start the services after creating them deprecated: false hidden: false experimental: false @@ -192,7 +192,7 @@ options: - option: quiet-pull value_type: bool default_value: "false" - description: Pull without printing progress information. + description: Pull without printing progress information deprecated: false hidden: false experimental: false @@ -202,7 +202,7 @@ options: - option: remove-orphans value_type: bool default_value: "false" - description: Remove containers for services not defined in the Compose file. + description: Remove containers for services not defined in the Compose file deprecated: false hidden: false experimental: false @@ -214,7 +214,7 @@ options: value_type: bool default_value: "false" description: | - Recreate anonymous volumes instead of retrieving data from the previous containers. + Recreate anonymous volumes instead of retrieving data from the previous containers deprecated: false hidden: false experimental: false @@ -237,7 +237,7 @@ options: value_type: int default_value: "0" description: | - Use this timeout in seconds for container shutdown when attached or when containers are already running. + Use this timeout in seconds for container shutdown when attached or when containers are already running deprecated: false hidden: false experimental: false @@ -247,7 +247,7 @@ options: - option: timestamps value_type: bool default_value: "false" - description: Show timestamps. + description: Show timestamps deprecated: false hidden: false experimental: false @@ -267,7 +267,19 @@ options: - option: wait-timeout value_type: int default_value: "0" - description: Maximum duration to wait for the project to be running|healthy. + description: Maximum duration to wait for the project to be running|healthy + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false + - option: watch + shorthand: w + value_type: bool + default_value: "false" + description: | + Watch source code and rebuild/refresh containers when files are updated. deprecated: false hidden: false experimental: false diff --git a/docs/reference/docker_compose_version.yaml b/docs/reference/docker_compose_version.yaml index b06c4b1508..789e94818e 100644 --- a/docs/reference/docker_compose_version.yaml +++ b/docs/reference/docker_compose_version.yaml @@ -18,7 +18,7 @@ options: - option: short value_type: bool default_value: "false" - description: Shows only Compose's version number. + description: Shows only Compose's version number deprecated: false hidden: false experimental: false diff --git a/go.mod b/go.mod index b5219731b3..890fa14167 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,15 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Microsoft/go-winio v0.6.1 github.com/buger/goterm v1.0.4 - github.com/compose-spec/compose-go/v2 v2.0.0-rc.3 + github.com/compose-spec/compose-go/v2 v2.0.0 github.com/containerd/console v1.0.3 github.com/containerd/containerd v1.7.12 github.com/davecgh/go-spew v1.1.1 github.com/distribution/reference v0.5.0 github.com/docker/buildx v0.12.0-rc2.0.20231219140829-617f538cb315 - github.com/docker/cli v25.0.1+incompatible + github.com/docker/cli v25.0.4-0.20240305161310-2bf4225ad269+incompatible github.com/docker/cli-docs-tool v0.6.0 - github.com/docker/docker v25.0.1+incompatible + github.com/docker/docker v25.0.4-0.20240301160236-51e876cd964c+incompatible github.com/docker/go-connections v0.5.0 github.com/docker/go-units v0.5.0 github.com/fsnotify/fsevents v0.1.1 @@ -23,13 +23,14 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/jonboulle/clockwork v0.4.0 github.com/mattn/go-shellwords v1.0.12 + github.com/mitchellh/go-ps v1.0.0 github.com/mitchellh/mapstructure v1.5.0 github.com/moby/buildkit v0.13.0-beta1.0.20231219135447-957cb50df991 github.com/moby/patternmatcher v0.6.0 github.com/moby/term v0.5.0 github.com/morikuni/aec v1.0.0 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0-rc5 + github.com/opencontainers/image-spec v1.1.0-rc6 github.com/otiai10/copy v1.14.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 @@ -37,6 +38,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/theupdateframework/notary v0.7.0 github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 go.opentelemetry.io/otel v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 @@ -44,10 +46,11 @@ require ( go.opentelemetry.io/otel/trace v1.19.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.4.0 - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 golang.org/x/sync v0.6.0 golang.org/x/sys v0.16.0 google.golang.org/grpc v1.59.0 + gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 ) @@ -145,7 +148,6 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 // indirect @@ -154,14 +156,14 @@ require ( go.opentelemetry.io/otel/metric v1.19.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.19.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/mod v0.11.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.10.0 // indirect + golang.org/x/tools v0.17.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect @@ -169,7 +171,6 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.26.7 // indirect k8s.io/apimachinery v0.26.7 // indirect k8s.io/apiserver v0.26.7 // indirect @@ -183,6 +184,8 @@ require ( tags.cncf.io/container-device-interface v0.6.2 // indirect ) -// Fix an issue with fsutil v0.0.0-20230825212630-f09800878302 on Windows -// See https://github.com/docker/buildx/issues/2207#issuecomment-1908460460 -replace github.com/tonistiigi/fsutil v0.0.0-20230825212630-f09800878302 => github.com/crazy-max/fsutil v0.0.0-20240124164449-376dc28ff40f +replace ( + // reverts https://github.com/moby/buildkit/pull/4094 to fix fsutil issues on Windows + github.com/moby/buildkit => github.com/crazy-max/buildkit v0.7.1-0.20240130133234-d9aa289bd124 // compose-957cb50df991 + github.com/tonistiigi/fsutil => github.com/tonistiigi/fsutil v0.0.0-20230629203738-36ef4d8c0dbb +) diff --git a/go.sum b/go.sum index d31a81011d..c23181627e 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.0.0-rc.3 h1:t0qajSNkH3zR4HEN2CM+GVU7GBx5AwqiYJk5w800M7w= -github.com/compose-spec/compose-go/v2 v2.0.0-rc.3/go.mod h1:r7CJHU0GaLtRVLm2ch8RCNkJh3GHyaqqc2rSti7VP44= +github.com/compose-spec/compose-go/v2 v2.0.0 h1:RLI8GmNxRLg759CzZITh/kGYZTYhEak121FaVYdXTC8= +github.com/compose-spec/compose-go/v2 v2.0.0/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= @@ -110,8 +110,8 @@ github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/crazy-max/fsutil v0.0.0-20240124164449-376dc28ff40f h1:f+sk5oEeSYL/2tjWraiDlR/JHJVwtqKYpGtMkfJ7MTc= -github.com/crazy-max/fsutil v0.0.0-20240124164449-376dc28ff40f/go.mod h1:9kMVqMyQ/Sx2df5LtnGG+nbrmiZzCS7V6gjW3oGHsvI= +github.com/crazy-max/buildkit v0.7.1-0.20240130133234-d9aa289bd124 h1:rTTqpfm06GSf2gjt8oo9LfUm2iGiYtx1VUDPfTHXqs4= +github.com/crazy-max/buildkit v0.7.1-0.20240130133234-d9aa289bd124/go.mod h1:eFZFY7VaoWWKmJLwkqmcWR6x0j8q+gXcngg3E4k0558= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -124,15 +124,15 @@ github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/buildx v0.12.0-rc2.0.20231219140829-617f538cb315 h1:UZxx9xBADdf/9UmSdEUi+pdJoPKpgcf9QUAY5gEIYmY= github.com/docker/buildx v0.12.0-rc2.0.20231219140829-617f538cb315/go.mod h1:X8ZHhuW6ncwtoJ36TlU+gyaROTcBkTE01VHYmTStQCE= -github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU= -github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v25.0.4-0.20240305161310-2bf4225ad269+incompatible h1:xhVCHXq+P5LhT31+RuDuk0xXEbEnd50Fr37J1bGuyWg= +github.com/docker/cli v25.0.4-0.20240305161310-2bf4225ad269+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli-docs-tool v0.6.0 h1:Z9x10SaZgFaB6jHgz3OWooynhSa40CsWkpe5hEnG/qA= github.com/docker/cli-docs-tool v0.6.0/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v25.0.1+incompatible h1:k5TYd5rIVQRSqcTwCID+cyVA0yRg86+Pcrz1ls0/frA= -github.com/docker/docker v25.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.4-0.20240301160236-51e876cd964c+incompatible h1:sCE9u4l5Kr3Z0pvUEAC6XKe/wnH6Q4O19I/0Mcqlxz8= +github.com/docker/docker v25.0.4-0.20240301160236-51e876cd964c+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= @@ -331,13 +331,13 @@ github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/buildkit v0.13.0-beta1.0.20231219135447-957cb50df991 h1:r80LLQ91uOLxU1ElAvrB1o8oBsph51lPzVnr7t2b200= -github.com/moby/buildkit v0.13.0-beta1.0.20231219135447-957cb50df991/go.mod h1:6MddWPSL5jxy+W8eMMHWDOfZzzRRKWXPZqajw72YHBc= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= @@ -382,8 +382,8 @@ github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= -github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU= +github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= @@ -472,6 +472,8 @@ github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4D github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= +github.com/tonistiigi/fsutil v0.0.0-20230629203738-36ef4d8c0dbb h1:uUe8rNyVXM8moActoBol6Xf6xX2GMr7SosR2EywMvGg= +github.com/tonistiigi/fsutil v0.0.0-20230629203738-36ef4d8c0dbb/go.mod h1:SxX/oNQ/ag6Vaoli547ipFK9J7BZn5JqJG0JE8lf8bA= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 h1:Y/M5lygoNPKwVNLMPXgVfsRT40CSFKXCxuU8LoHySjs= @@ -537,19 +539,19 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -564,8 +566,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= @@ -606,8 +608,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -626,8 +628,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/desktop/client.go b/internal/desktop/client.go new file mode 100644 index 0000000000..e43a10a096 --- /dev/null +++ b/internal/desktop/client.go @@ -0,0 +1,93 @@ +/* + Copyright 2024 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 desktop + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + + "github.com/docker/compose/v2/internal/memnet" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +// Client for integration with Docker Desktop features. +type Client struct { + client *http.Client +} + +// NewClient creates a Desktop integration client for the provided in-memory +// socket address (AF_UNIX or named pipe). +func NewClient(apiEndpoint string) *Client { + var transport http.RoundTripper = &http.Transport{ + DisableCompression: true, + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return memnet.DialEndpoint(ctx, apiEndpoint) + }, + } + transport = otelhttp.NewTransport(transport) + + c := &Client{ + client: &http.Client{Transport: transport}, + } + return c +} + +// Close releases any open connections. +func (c *Client) Close() error { + c.client.CloseIdleConnections() + return nil +} + +type PingResponse struct { + ServerTime int64 `json:"serverTime"` +} + +// Ping is a minimal API used to ensure that the server is available. +func (c *Client) Ping(ctx context.Context) (*PingResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, backendURL("/ping"), http.NoBody) + if err != nil { + return nil, err + } + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer func() { + _ = resp.Body.Close() + }() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var ret PingResponse + if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { + return nil, err + } + return &ret, nil +} + +// backendURL generates a URL for the given API path. +// +// NOTE: Custom transport handles communication. The host is to create a valid +// URL for the Go http.Client that is also descriptive in error/logs. +func backendURL(path string) string { + return "http://docker-desktop/" + strings.TrimPrefix(path, "/") +} diff --git a/internal/desktop/integration.go b/internal/desktop/integration.go new file mode 100644 index 0000000000..62dd4b9315 --- /dev/null +++ b/internal/desktop/integration.go @@ -0,0 +1,25 @@ +/* + Copyright 2024 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 desktop + +import ( + "context" +) + +type IntegrationService interface { + MaybeEnableDesktopIntegration(ctx context.Context) error +} diff --git a/internal/locker/pidfile.go b/internal/locker/pidfile.go index ea688fb66f..08dcea1f3a 100644 --- a/internal/locker/pidfile.go +++ b/internal/locker/pidfile.go @@ -18,10 +18,7 @@ package locker import ( "fmt" - "os" "path/filepath" - - "github.com/docker/docker/pkg/pidfile" ) type Pidfile struct { @@ -36,7 +33,3 @@ func NewPidfile(projectName string) (*Pidfile, error) { path := filepath.Join(run, fmt.Sprintf("%s.pid", projectName)) return &Pidfile{path: path}, nil } - -func (f *Pidfile) Lock() error { - return pidfile.Write(f.path, os.Getpid()) -} diff --git a/internal/locker/pidfile_unix.go b/internal/locker/pidfile_unix.go new file mode 100644 index 0000000000..484b65d825 --- /dev/null +++ b/internal/locker/pidfile_unix.go @@ -0,0 +1,29 @@ +//go:build !windows + +/* + 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 locker + +import ( + "os" + + "github.com/docker/docker/pkg/pidfile" +) + +func (f *Pidfile) Lock() error { + return pidfile.Write(f.path, os.Getpid()) +} diff --git a/internal/locker/pidfile_windows.go b/internal/locker/pidfile_windows.go new file mode 100644 index 0000000000..9f8d4c3ee4 --- /dev/null +++ b/internal/locker/pidfile_windows.go @@ -0,0 +1,47 @@ +//go:build windows + +/* + 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 locker + +import ( + "github.com/docker/docker/pkg/pidfile" + "github.com/mitchellh/go-ps" + "os" +) + +func (f *Pidfile) Lock() error { + newPID := os.Getpid() + err := pidfile.Write(f.path, newPID) + if err != nil { + // Get PID registered in the file + pid, errPid := pidfile.Read(f.path) + if errPid != nil { + return err + } + // Some users faced issues on Windows where the process written in the pidfile was identified as still existing + // So we used a 2nd process library to verify if this not a false positive feedback + // Check if the process exists + process, errPid := ps.FindProcess(pid) + if process == nil && errPid == nil { + // If the process does not exist, remove the pidfile and try to lock again + _ = os.Remove(f.path) + return pidfile.Write(f.path, newPID) + } + } + return err +} diff --git a/internal/memnet/conn.go b/internal/memnet/conn.go new file mode 100644 index 0000000000..224bec7883 --- /dev/null +++ b/internal/memnet/conn.go @@ -0,0 +1,50 @@ +/* + Copyright 2020 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 memnet + +import ( + "context" + "fmt" + "net" + "strings" +) + +func DialEndpoint(ctx context.Context, endpoint string) (net.Conn, error) { + if addr, ok := strings.CutPrefix(endpoint, "unix://"); ok { + return Dial(ctx, "unix", addr) + } + if addr, ok := strings.CutPrefix(endpoint, "npipe://"); ok { + return Dial(ctx, "npipe", addr) + } + return nil, fmt.Errorf("unsupported protocol for address: %s", endpoint) +} + +func Dial(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + switch network { + case "unix": + if err := validateSocketPath(addr); err != nil { + return nil, err + } + return d.DialContext(ctx, "unix", addr) + case "npipe": + // N.B. this will return an error on non-Windows + return dialNamedPipe(ctx, addr) + default: + return nil, fmt.Errorf("unsupported network: %s", network) + } +} diff --git a/internal/tracing/conn_unix.go b/internal/memnet/conn_unix.go similarity index 64% rename from internal/tracing/conn_unix.go rename to internal/memnet/conn_unix.go index 78294f4beb..e151984848 100644 --- a/internal/tracing/conn_unix.go +++ b/internal/memnet/conn_unix.go @@ -16,29 +16,24 @@ limitations under the License. */ -package tracing +package memnet import ( "context" "fmt" "net" - "strings" "syscall" ) const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path) -func DialInMemory(ctx context.Context, addr string) (net.Conn, error) { - if !strings.HasPrefix(addr, "unix://") { - return nil, fmt.Errorf("not a Unix socket address: %s", addr) - } - addr = strings.TrimPrefix(addr, "unix://") +func dialNamedPipe(_ context.Context, _ string) (net.Conn, error) { + return nil, fmt.Errorf("named pipes are only available on Windows") +} +func validateSocketPath(addr string) error { if len(addr) > maxUnixSocketPathSize { - //goland:noinspection GoErrorStringFormat - return nil, fmt.Errorf("Unix socket address is too long: %s", addr) + return fmt.Errorf("socket address is too long: %s", addr) } - - var d net.Dialer - return d.DialContext(ctx, "unix", addr) + return nil } diff --git a/internal/tracing/conn_windows.go b/internal/memnet/conn_windows.go similarity index 73% rename from internal/tracing/conn_windows.go rename to internal/memnet/conn_windows.go index 30deaa464d..b7f7d9ea8f 100644 --- a/internal/tracing/conn_windows.go +++ b/internal/memnet/conn_windows.go @@ -14,22 +14,20 @@ limitations under the License. */ -package tracing +package memnet import ( "context" - "fmt" "net" - "strings" "github.com/Microsoft/go-winio" ) -func DialInMemory(ctx context.Context, addr string) (net.Conn, error) { - if !strings.HasPrefix(addr, "npipe://") { - return nil, fmt.Errorf("not a named pipe address: %s", addr) - } - addr = strings.TrimPrefix(addr, "npipe://") - +func dialNamedPipe(ctx context.Context, addr string) (net.Conn, error) { return winio.DialPipeContext(ctx, addr) } + +func validateSocketPath(addr string) error { + // AF_UNIX sockets do not have strict path limits on Windows + return nil +} diff --git a/internal/sync/docker_cp.go b/internal/sync/docker_cp.go deleted file mode 100644 index 47077b404e..0000000000 --- a/internal/sync/docker_cp.go +++ /dev/null @@ -1,104 +0,0 @@ -/* - 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 sync - -import ( - "context" - "errors" - "fmt" - "io" - "io/fs" - "os" - - "github.com/compose-spec/compose-go/v2/types" - "github.com/docker/compose/v2/pkg/api" - "github.com/sirupsen/logrus" -) - -type ComposeClient interface { - Exec(ctx context.Context, projectName string, options api.RunOptions) (int, error) - - Copy(ctx context.Context, projectName string, options api.CopyOptions) error -} - -type DockerCopy struct { - client ComposeClient - - projectName string - - infoWriter io.Writer -} - -var _ Syncer = &DockerCopy{} - -func NewDockerCopy(projectName string, client ComposeClient, infoWriter io.Writer) *DockerCopy { - return &DockerCopy{ - projectName: projectName, - client: client, - infoWriter: infoWriter, - } -} - -func (d *DockerCopy) Sync(ctx context.Context, service types.ServiceConfig, paths []PathMapping) error { - var errs []error - for i := range paths { - if err := d.sync(ctx, service, paths[i]); err != nil { - errs = append(errs, err) - } - } - return errors.Join(errs...) -} - -func (d *DockerCopy) sync(ctx context.Context, service types.ServiceConfig, pathMapping PathMapping) error { - scale := service.GetScale() - - if fi, statErr := os.Stat(pathMapping.HostPath); statErr == nil { - if fi.IsDir() { - for i := 1; i <= scale; i++ { - _, err := d.client.Exec(ctx, d.projectName, api.RunOptions{ - Service: service.Name, - Command: []string{"mkdir", "-p", pathMapping.ContainerPath}, - Index: i, - }) - if err != nil { - logrus.Warnf("failed to create %q from %s: %v", pathMapping.ContainerPath, service.Name, err) - } - } - fmt.Fprintf(d.infoWriter, "%s created\n", pathMapping.ContainerPath) - } else { - err := d.client.Copy(ctx, d.projectName, api.CopyOptions{ - Source: pathMapping.HostPath, - Destination: fmt.Sprintf("%s:%s", service.Name, pathMapping.ContainerPath), - }) - if err != nil { - return err - } - fmt.Fprintf(d.infoWriter, "%s updated\n", pathMapping.ContainerPath) - } - } else if errors.Is(statErr, fs.ErrNotExist) { - for i := 1; i <= scale; i++ { - _, err := d.client.Exec(ctx, d.projectName, api.RunOptions{ - Service: service.Name, - Command: []string{"rm", "-rf", pathMapping.ContainerPath}, - Index: i, - }) - if err != nil { - logrus.Warnf("failed to delete %q from %s: %v", pathMapping.ContainerPath, service.Name, err) - } - } - fmt.Fprintf(d.infoWriter, "%s deleted from service\n", pathMapping.ContainerPath) - } - return nil -} diff --git a/internal/sync/writer.go b/internal/sync/writer.go deleted file mode 100644 index f5c182d1bf..0000000000 --- a/internal/sync/writer.go +++ /dev/null @@ -1,91 +0,0 @@ -/* - 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 sync - -import ( - "errors" - "io" -) - -// lossyMultiWriter attempts to tee all writes to the provided io.PipeWriter -// instances. -// -// If a writer fails during a Write call, the write-side of the pipe is then -// closed with the error and no subsequent attempts are made to write to the -// pipe. -// -// If all writers fail during a write, an error is returned. -// -// On Close, any remaining writers are closed. -type lossyMultiWriter struct { - writers []*io.PipeWriter -} - -// newLossyMultiWriter creates a new writer that *attempts* to tee all data written to it to the provided io.PipeWriter -// instances. Rather than failing a write operation if any writer fails, writes only fail if there are no more valid -// writers. Otherwise, errors for specific writers are propagated via CloseWithError. -func newLossyMultiWriter(writers ...*io.PipeWriter) *lossyMultiWriter { - // reverse the writers because during the write we iterate - // backwards, so this way we'll end up writing in the same - // order as the writers were passed to us - writers = append([]*io.PipeWriter(nil), writers...) - for i, j := 0, len(writers)-1; i < j; i, j = i+1, j-1 { - writers[i], writers[j] = writers[j], writers[i] - } - - return &lossyMultiWriter{ - writers: writers, - } -} - -// Write writes to each writer that is still active (i.e. has not failed/encountered an error on write). -// -// If a writer encounters an error during the write, the write side of the pipe is closed with the error -// and no subsequent attempts will be made to write to that writer. -// -// An error is only returned from this function if ALL writers have failed. -func (l *lossyMultiWriter) Write(p []byte) (int, error) { - // NOTE: this function iterates backwards so that it can - // safely remove elements during the loop - for i := len(l.writers) - 1; i >= 0; i-- { - written, err := l.writers[i].Write(p) - if err == nil && written != len(p) { - err = io.ErrShortWrite - } - if err != nil { - // pipe writer close cannot fail - _ = l.writers[i].CloseWithError(err) - l.writers = append(l.writers[:i], l.writers[i+1:]...) - } - } - - if len(l.writers) == 0 { - return 0, errors.New("no writers remaining") - } - - return len(p), nil -} - -// Close closes any still open (non-failed) writers. -// -// Failed writers have already been closed with an error. -func (l *lossyMultiWriter) Close() { - for i := range l.writers { - // pipe writer close cannot fail - _ = l.writers[i].Close() - } -} diff --git a/internal/sync/writer_test.go b/internal/sync/writer_test.go deleted file mode 100644 index b6de694c72..0000000000 --- a/internal/sync/writer_test.go +++ /dev/null @@ -1,152 +0,0 @@ -/* - 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 sync - -import ( - "context" - "errors" - "io" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestLossyMultiWriter(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - const count = 5 - readers := make([]*bufReader, count) - writers := make([]*io.PipeWriter, count) - for i := 0; i < count; i++ { - r, w := io.Pipe() - readers[i] = newBufReader(ctx, r) - writers[i] = w - } - - w := newLossyMultiWriter(writers...) - t.Cleanup(w.Close) - n, err := w.Write([]byte("hello world")) - require.Equal(t, 11, n) - require.NoError(t, err) - for i := range readers { - readers[i].waitForWrite(t) - require.Equal(t, "hello world", string(readers[i].contents())) - readers[i].reset() - } - - // even if a writer fails (in this case simulated by closing the receiving end of the pipe), - // write operations should continue to return nil error but the writer should be closed - // with an error - const failIndex = 3 - require.NoError(t, readers[failIndex].r.CloseWithError(errors.New("oh no"))) - n, err = w.Write([]byte("hello")) - require.Equal(t, 5, n) - require.NoError(t, err) - for i := range readers { - readers[i].waitForWrite(t) - if i == failIndex { - err := readers[i].error() - require.EqualError(t, err, "io: read/write on closed pipe") - require.Empty(t, readers[i].contents()) - } else { - require.Equal(t, "hello", string(readers[i].contents())) - } - } - - // perform another write, verify there's still no errors - n, err = w.Write([]byte(" world")) - require.Equal(t, 6, n) - require.NoError(t, err) -} - -type bufReader struct { - ctx context.Context - r *io.PipeReader - mu sync.Mutex - err error - data []byte - writeSync chan struct{} -} - -func newBufReader(ctx context.Context, r *io.PipeReader) *bufReader { - b := &bufReader{ - ctx: ctx, - r: r, - writeSync: make(chan struct{}), - } - go b.consume() - return b -} - -func (b *bufReader) waitForWrite(t testing.TB) { - t.Helper() - select { - case <-b.writeSync: - return - case <-time.After(50 * time.Millisecond): - t.Fatal("timed out waiting for write") - } -} - -func (b *bufReader) consume() { - defer close(b.writeSync) - for { - buf := make([]byte, 512) - n, err := b.r.Read(buf) - if n != 0 { - b.mu.Lock() - b.data = append(b.data, buf[:n]...) - b.mu.Unlock() - } - if errors.Is(err, io.EOF) { - return - } - if err != nil { - b.mu.Lock() - b.err = err - b.mu.Unlock() - return - } - // prevent goroutine leak, tie lifetime to the test - select { - case b.writeSync <- struct{}{}: - case <-b.ctx.Done(): - return - } - } -} - -func (b *bufReader) contents() []byte { - b.mu.Lock() - defer b.mu.Unlock() - return b.data -} - -func (b *bufReader) reset() { - b.mu.Lock() - defer b.mu.Unlock() - b.data = nil -} - -func (b *bufReader) error() error { - b.mu.Lock() - defer b.mu.Unlock() - return b.err -} diff --git a/internal/tracing/attributes.go b/internal/tracing/attributes.go index 1fd2dd7f91..3d27c4796a 100644 --- a/internal/tracing/attributes.go +++ b/internal/tracing/attributes.go @@ -17,11 +17,13 @@ package tracing import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" "strings" "time" - "github.com/docker/compose/v2/pkg/utils" - "github.com/compose-spec/compose-go/v2/types" moby "github.com/docker/docker/api/types" "go.opentelemetry.io/otel/attribute" @@ -32,6 +34,14 @@ import ( // downstream functions that accept slices of trace.SpanStartOption and trace.EventOption. type SpanOptions []trace.SpanStartEventOption +type MetricsKey struct{} + +type Metrics struct { + CountExtends int + CountIncludesLocal int + CountIncludesRemote int +} + func (s SpanOptions) SpanStartOptions() []trace.SpanStartOption { out := make([]trace.SpanStartOption, len(s)) for i := range s { @@ -53,24 +63,37 @@ func (s SpanOptions) EventOptions() []trace.EventOption { // For convenience, it's returned as a SpanOptions object to allow it to be // passed directly to the wrapping helper methods in this package such as // SpanWrapFunc. -func ProjectOptions(proj *types.Project) SpanOptions { +func ProjectOptions(ctx context.Context, proj *types.Project) SpanOptions { if proj == nil { return nil } - + capabilities, gpu, tpu := proj.ServicesWithCapabilities() attrs := []attribute.KeyValue{ attribute.String("project.name", proj.Name), attribute.String("project.dir", proj.WorkingDir), attribute.StringSlice("project.compose_files", proj.ComposeFiles), - attribute.StringSlice("project.services.active", proj.ServiceNames()), - attribute.StringSlice("project.services.disabled", proj.DisabledServiceNames()), attribute.StringSlice("project.profiles", proj.Profiles), attribute.StringSlice("project.volumes", proj.VolumeNames()), attribute.StringSlice("project.networks", proj.NetworkNames()), attribute.StringSlice("project.secrets", proj.SecretNames()), attribute.StringSlice("project.configs", proj.ConfigNames()), attribute.StringSlice("project.extensions", keys(proj.Extensions)), - attribute.StringSlice("project.includes", flattenIncludeReferences(proj.IncludeReferences)), + attribute.StringSlice("project.services.active", proj.ServiceNames()), + attribute.StringSlice("project.services.disabled", proj.DisabledServiceNames()), + attribute.StringSlice("project.services.build", proj.ServicesWithBuild()), + attribute.StringSlice("project.services.depends_on", proj.ServicesWithDependsOn()), + attribute.StringSlice("project.services.capabilities", capabilities), + attribute.StringSlice("project.services.capabilities.gpu", gpu), + attribute.StringSlice("project.services.capabilities.tpu", tpu), + } + if metrics, ok := ctx.Value(MetricsKey{}).(Metrics); ok { + attrs = append(attrs, attribute.Int("project.services.extends", metrics.CountExtends)) + attrs = append(attrs, attribute.Int("project.includes.local", metrics.CountIncludesLocal)) + attrs = append(attrs, attribute.Int("project.includes.remote", metrics.CountIncludesRemote)) + } + + if projHash, ok := projectHash(proj); ok { + attrs = append(attrs, attribute.String("project.hash", projHash)) } return []trace.SpanStartEventOption{ trace.WithAttributes(attrs...), @@ -149,12 +172,22 @@ func unixTimeAttr(key string, value int64) attribute.KeyValue { return timeAttr(key, time.Unix(value, 0).UTC()) } -func flattenIncludeReferences(includeRefs map[string][]types.IncludeConfig) []string { - ret := utils.NewSet[string]() - for _, included := range includeRefs { - for i := range included { - ret.AddAll(included[i].Path...) - } +// projectHash returns a checksum from the JSON encoding of the project. +func projectHash(p *types.Project) (string, bool) { + if p == nil { + return "", false + } + // disabled services aren't included in the output, so make a copy with + // all the services active for hashing + var err error + p, err = p.WithServicesEnabled(append(p.ServiceNames(), p.DisabledServiceNames()...)...) + if err != nil { + return "", false + } + projData, err := json.Marshal(p) + if err != nil { + return "", false } - return ret.Elements() + d := sha256.Sum256(projData) + return fmt.Sprintf("%x", d), true } diff --git a/internal/tracing/attributes_test.go b/internal/tracing/attributes_test.go new file mode 100644 index 0000000000..d4277a940a --- /dev/null +++ b/internal/tracing/attributes_test.go @@ -0,0 +1,67 @@ +/* + Copyright 2024 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 tracing + +import ( + "testing" + + "github.com/compose-spec/compose-go/v2/types" + "github.com/stretchr/testify/require" +) + +func TestProjectHash(t *testing.T) { + projA := &types.Project{ + Name: "fake-proj", + WorkingDir: "/tmp", + Services: map[string]types.ServiceConfig{ + "foo": {Image: "fake-image"}, + }, + DisabledServices: map[string]types.ServiceConfig{ + "bar": {Image: "diff-image"}, + }, + } + projB := &types.Project{ + Name: "fake-proj", + WorkingDir: "/tmp", + Services: map[string]types.ServiceConfig{ + "foo": {Image: "fake-image"}, + "bar": {Image: "diff-image"}, + }, + } + projC := &types.Project{ + Name: "fake-proj", + WorkingDir: "/tmp", + Services: map[string]types.ServiceConfig{ + "foo": {Image: "fake-image"}, + "bar": {Image: "diff-image"}, + "baz": {Image: "yet-another-image"}, + }, + } + + hashA, ok := projectHash(projA) + require.True(t, ok) + require.NotEmpty(t, hashA) + hashB, ok := projectHash(projB) + require.True(t, ok) + require.NotEmpty(t, hashB) + require.Equal(t, hashA, hashB) + + hashC, ok := projectHash(projC) + require.True(t, ok) + require.NotEmpty(t, hashC) + require.NotEqual(t, hashC, hashA) +} diff --git a/internal/tracing/docker_context.go b/internal/tracing/docker_context.go index f5f5ece3f7..229e77477d 100644 --- a/internal/tracing/docker_context.go +++ b/internal/tracing/docker_context.go @@ -24,6 +24,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/context/store" + "github.com/docker/compose/v2/internal/memnet" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "google.golang.org/grpc" @@ -67,7 +68,9 @@ func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptr conn, err := grpc.DialContext( dialCtx, cfg.Endpoint, - grpc.WithContextDialer(DialInMemory), + grpc.WithContextDialer(memnet.DialEndpoint), + // this dial is restricted to using a local Unix socket / named pipe, + // so there is no need for TLS grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { diff --git a/pkg/api/api.go b/pkg/api/api.go index 3b8f091611..641840a3e5 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -19,6 +19,7 @@ package api import ( "context" "fmt" + "io" "strings" "time" @@ -52,8 +53,6 @@ type Service interface { Ps(ctx context.Context, projectName string, options PsOptions) ([]ContainerSummary, error) // List executes the equivalent to a `docker stack ls` List(ctx context.Context, options ListOptions) ([]Stack, error) - // Config executes the equivalent to a `compose config` - Config(ctx context.Context, project *types.Project, options ConfigOptions) ([]byte, error) // Kill executes the equivalent to a `compose kill` Kill(ctx context.Context, projectName string, options KillOptions) error // RunOneOffContainer creates a service oneoff container and starts its dependencies @@ -116,9 +115,13 @@ type VizOptions struct { Indentation string } +// WatchLogger is a reserved name to log watch events +const WatchLogger = "#watch" + // WatchOptions group options of the Watch API type WatchOptions struct { - Build BuildOptions + Build *BuildOptions + LogTo LogConsumer } // BuildOptions group options of the Build API @@ -145,6 +148,8 @@ type BuildOptions struct { Memory int64 // Builder name passed in the command line Builder string + // OutPrinter used for printing the progress output + OutPrinter io.Writer } // Apply mutates project according to build options @@ -216,6 +221,7 @@ type StartOptions struct { WaitTimeout time.Duration // Services passed in the command line to be started Services []string + Watch bool } // RestartOptions group options of the Restart API diff --git a/pkg/compose/build.go b/pkg/compose/build.go index b4ce4c80aa..a6ce19a12d 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -20,12 +20,14 @@ import ( "context" "errors" "fmt" + "io" "os" "path/filepath" "github.com/moby/buildkit/util/progress/progressui" "github.com/compose-spec/compose-go/v2/types" + "github.com/containerd/console" "github.com/containerd/containerd/platforms" "github.com/docker/buildx/build" "github.com/docker/buildx/builder" @@ -127,7 +129,15 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti progressCtx, cancel := context.WithCancel(context.Background()) defer cancel() - w, err = xprogress.NewPrinter(progressCtx, os.Stdout, progressui.DisplayMode(options.Progress), + var outPrinter io.Writer = os.Stdout + if options.OutPrinter != nil { + outPrinter = options.OutPrinter + } + + if options.Quiet { + options.Progress = progress.ModeQuiet + } + w, err = xprogress.NewPrinter(progressCtx, progressPrinter(outPrinter), progressui.DisplayMode(options.Progress), xprogress.WithDesc( fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver), fmt.Sprintf("%s:%s", b.Driver, b.Name), @@ -170,7 +180,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti } if options.Memory != 0 { - fmt.Fprintln(s.stderr(), "WARNING: --memory is not supported by BuildKit and will be ignored.") + fmt.Fprintln(s.stderr(), "WARNING: --memory is not supported by BuildKit and will be ignored") } buildOptions, err := s.toBuildOptions(project, service, options) @@ -221,7 +231,7 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types. return err } - err = tracing.SpanWrapFunc("project/pull", tracing.ProjectOptions(project), + err = tracing.SpanWrapFunc("project/pull", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error { return s.pullRequiredImages(ctx, project, images, quietPull) }, @@ -231,7 +241,7 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types. } if buildOpts != nil { - err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(project), + err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error { builtImages, err := s.build(ctx, project, *buildOpts, images) if err != nil { @@ -564,3 +574,29 @@ func parsePlatforms(service types.ServiceConfig) ([]specs.Platform, error) { return ret, nil } + +type pPrinter struct { + io.Writer +} + +func (p *pPrinter) Read(_ []byte) (n int, err error) { + return 0, errors.New("not implemented") +} + +func (p *pPrinter) Close() error { + return nil +} + +func (p *pPrinter) Fd() uintptr { + return 0 +} + +func (p *pPrinter) Name() string { + return "pPrinter" +} + +func progressPrinter(w io.Writer) console.File { + return &pPrinter{ + Writer: w, + } +} diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index 9230e8091c..fa631bfadb 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -18,6 +18,7 @@ package compose import ( "context" + "errors" "fmt" "io" "os" @@ -25,12 +26,11 @@ import ( "strings" "sync" - "github.com/jonboulle/clockwork" - + "github.com/docker/compose/v2/internal/desktop" "github.com/docker/docker/api/types/volume" + "github.com/jonboulle/clockwork" "github.com/compose-spec/compose-go/v2/types" - "github.com/distribution/reference" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/flags" @@ -40,7 +40,6 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" - "github.com/opencontainers/go-digest" ) var stdioToStdout bool @@ -63,12 +62,29 @@ func NewComposeService(dockerCli command.Cli) api.Service { } type composeService struct { - dockerCli command.Cli + dockerCli command.Cli + desktopCli *desktop.Client + clock clockwork.Clock maxConcurrency int dryRun bool } +// Close releases any connections/resources held by the underlying clients. +// +// In practice, this service has the same lifetime as the process, so everything +// will get cleaned up at about the same time regardless even if not invoked. +func (s *composeService) Close() error { + var errs []error + if s.dockerCli != nil { + errs = append(errs, s.dockerCli.Client().Close()) + } + if s.desktopCli != nil { + errs = append(errs, s.desktopCli.Close()) + } + return errors.Join(errs...) +} + func (s *composeService) apiClient() client.APIClient { return s.dockerCli.Client() } @@ -132,7 +148,8 @@ func getCanonicalContainerName(c moby.Container) string { return name[1:] } } - return c.Names[0][1:] + + return strings.TrimPrefix(c.Names[0], "/") } func getContainerNameWithoutProject(c moby.Container) string { @@ -146,35 +163,6 @@ func getContainerNameWithoutProject(c moby.Container) string { return name[len(project)+1:] } -func (s *composeService) Config(ctx context.Context, project *types.Project, options api.ConfigOptions) ([]byte, error) { - if options.ResolveImageDigests { - var err error - project, err = project.WithImagesResolved(func(named reference.Named) (digest.Digest, error) { - auth, err := encodedAuth(named, s.configFile()) - if err != nil { - return "", err - } - inspect, err := s.apiClient().DistributionInspect(ctx, named.String(), auth) - if err != nil { - return "", err - } - return inspect.Descriptor.Digest, nil - }) - if err != nil { - return nil, err - } - } - - switch options.Format { - case "json": - return project.MarshalJSON() - case "yaml": - return project.MarshalYAML() - default: - return nil, fmt.Errorf("unsupported format %q", options.Format) - } -} - // projectFromName builds a types.Project based on actual resources with compose labels set func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) { project := &types.Project{ @@ -311,11 +299,13 @@ func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) { return swarmEnabled.val, swarmEnabled.err } -var runtimeVersion = struct { +type runtimeVersionCache struct { once sync.Once val string err error -}{} +} + +var runtimeVersion runtimeVersionCache func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) { runtimeVersion.once.Do(func() { diff --git a/pkg/compose/containers.go b/pkg/compose/containers.go index ce17228926..cd75eeff56 100644 --- a/pkg/compose/containers.go +++ b/pkg/compose/containers.go @@ -127,13 +127,7 @@ func isNotService(services ...string) containerPredicate { // isOrphaned is a predicate to select containers without a matching service definition in compose project func isOrphaned(project *types.Project) containerPredicate { - var services []string - for _, s := range project.Services { - services = append(services, s.Name) - } - for _, s := range project.DisabledServices { - services = append(services, s.Name) - } + services := append(project.ServiceNames(), project.DisabledServiceNames()...) return func(c moby.Container) bool { service := c.Labels[api.ServiceLabel] return !utils.StringContains(services, service) diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index f65247b086..6fb6f3e3c9 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -34,6 +34,7 @@ import ( "github.com/docker/compose/v2/internal/tracing" moby "github.com/docker/docker/api/types" containerType "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/versions" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" @@ -126,6 +127,24 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project, } sort.Slice(containers, func(i, j int) bool { + // select obsolete containers first, so they get removed as we scale down + if obsolete, _ := mustRecreate(service, containers[i], recreate); obsolete { + // i is obsolete, so must be first in the list + return true + } + if obsolete, _ := mustRecreate(service, containers[j], recreate); obsolete { + // j is obsolete, so must be first in the list + return false + } + + // For up-to-date containers, sort by container number to preserve low-values in container numbers + ni, erri := strconv.Atoi(containers[i].Labels[api.ContainerNumberLabel]) + nj, errj := strconv.Atoi(containers[j].Labels[api.ContainerNumberLabel]) + if erri == nil && errj == nil { + return ni < nj + } + + // If we don't get a container number (?) just sort by creation date return containers[i].Created < containers[j].Created }) for i, container := range containers { @@ -600,19 +619,27 @@ func (s *composeService) createMobyContainer(ctx context.Context, }, } - // the highest-priority network is the primary and is included in the ContainerCreate API - // call via container.NetworkMode & network.NetworkingConfig - // any remaining networks are connected one-by-one here after creation (but before start) - serviceNetworks := service.NetworksByPriority() - for _, networkKey := range serviceNetworks { - mobyNetworkName := project.Networks[networkKey].Name - if string(cfgs.Host.NetworkMode) == mobyNetworkName { - // primary network already configured as part of ContainerCreate - continue - } - epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases) - if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil { - return created, err + apiVersion, err := s.RuntimeVersion(ctx) + if err != nil { + return created, err + } + // Starting API version 1.44, the ContainerCreate API call takes multiple networks + // so we include all the configurations there and can skip the one-by-one calls here + if versions.LessThan(apiVersion, "1.44") { + // the highest-priority network is the primary and is included in the ContainerCreate API + // call via container.NetworkMode & network.NetworkingConfig + // any remaining networks are connected one-by-one here after creation (but before start) + serviceNetworks := service.NetworksByPriority() + for _, networkKey := range serviceNetworks { + mobyNetworkName := project.Networks[networkKey].Name + if string(cfgs.Host.NetworkMode) == mobyNetworkName { + // primary network already configured as part of ContainerCreate + continue + } + epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases) + if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil { + return created, err + } } } diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go index 32917cb661..e25ccd9f64 100644 --- a/pkg/compose/convergence_test.go +++ b/pkg/compose/convergence_test.go @@ -23,14 +23,18 @@ import ( "testing" "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/cli/cli/config/configfile" moby "github.com/docker/docker/api/types" containerType "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/go-connections/nat" "go.uber.org/mock/gomock" "gotest.tools/v3/assert" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/mocks" + "github.com/docker/compose/v2/pkg/progress" ) func TestContainerName(t *testing.T) { @@ -251,3 +255,182 @@ func TestWaitDependencies(t *testing.T) { assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil)) }) } + +func TestCreateMobyContainer(t *testing.T) { + t.Run("connects container networks one by one if API <1.44", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + apiClient := mocks.NewMockAPIClient(mockCtrl) + cli := mocks.NewMockCli(mockCtrl) + tested := composeService{ + dockerCli: cli, + } + cli.EXPECT().Client().Return(apiClient).AnyTimes() + cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes() + apiClient.EXPECT().DaemonHost().Return("").AnyTimes() + apiClient.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(moby.ImageInspect{}, nil, nil).AnyTimes() + // force `RuntimeVersion` to fetch again + runtimeVersion = runtimeVersionCache{} + apiClient.EXPECT().ServerVersion(gomock.Any()).Return(moby.Version{ + APIVersion: "1.43", + }, nil).AnyTimes() + + service := types.ServiceConfig{ + Name: "test", + Networks: map[string]*types.ServiceNetworkConfig{ + "a": { + Priority: 10, + }, + "b": { + Priority: 100, + }, + }, + } + project := types.Project{ + Name: "bork", + Services: types.Services{ + "test": service, + }, + Networks: types.Networks{ + "a": types.NetworkConfig{ + Name: "a-moby-name", + }, + "b": types.NetworkConfig{ + Name: "b-moby-name", + }, + }, + } + + var falseBool bool + apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Eq( + &containerType.HostConfig{ + PortBindings: nat.PortMap{}, + ExtraHosts: []string{}, + Tmpfs: map[string]string{}, + Resources: containerType.Resources{ + OomKillDisable: &falseBool, + }, + NetworkMode: "b-moby-name", + }), gomock.Eq( + &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + "b-moby-name": { + IPAMConfig: &network.EndpointIPAMConfig{}, + Aliases: []string{"bork-test-0"}, + }, + }, + }), gomock.Any(), gomock.Any()).Times(1).Return( + containerType.CreateResponse{ + ID: "an-id", + }, nil) + + apiClient.EXPECT().ContainerInspect(gomock.Any(), gomock.Eq("an-id")).Times(1).Return( + moby.ContainerJSON{ + ContainerJSONBase: &moby.ContainerJSONBase{ + ID: "an-id", + Name: "a-name", + }, + Config: &containerType.Config{}, + NetworkSettings: &moby.NetworkSettings{}, + }, nil) + + apiClient.EXPECT().NetworkConnect(gomock.Any(), "a-moby-name", "an-id", gomock.Eq( + &network.EndpointSettings{ + IPAMConfig: &network.EndpointIPAMConfig{}, + Aliases: []string{"bork-test-0"}, + })) + + _, err := tested.createMobyContainer(context.Background(), &project, service, "test", 0, nil, createOptions{ + Labels: make(types.Labels), + }, progress.ContextWriter(context.TODO())) + assert.NilError(t, err) + }) + + t.Run("includes all container networks in ContainerCreate call if API >=1.44", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + apiClient := mocks.NewMockAPIClient(mockCtrl) + cli := mocks.NewMockCli(mockCtrl) + tested := composeService{ + dockerCli: cli, + } + cli.EXPECT().Client().Return(apiClient).AnyTimes() + cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes() + apiClient.EXPECT().DaemonHost().Return("").AnyTimes() + apiClient.EXPECT().ImageInspectWithRaw(gomock.Any(), gomock.Any()).Return(moby.ImageInspect{}, nil, nil).AnyTimes() + // force `RuntimeVersion` to fetch fresh version + runtimeVersion = runtimeVersionCache{} + apiClient.EXPECT().ServerVersion(gomock.Any()).Return(moby.Version{ + APIVersion: "1.44", + }, nil).AnyTimes() + + service := types.ServiceConfig{ + Name: "test", + Networks: map[string]*types.ServiceNetworkConfig{ + "a": { + Priority: 10, + }, + "b": { + Priority: 100, + }, + }, + } + project := types.Project{ + Name: "bork", + Services: types.Services{ + "test": service, + }, + Networks: types.Networks{ + "a": types.NetworkConfig{ + Name: "a-moby-name", + }, + "b": types.NetworkConfig{ + Name: "b-moby-name", + }, + }, + } + + var falseBool bool + apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Eq( + &containerType.HostConfig{ + PortBindings: nat.PortMap{}, + ExtraHosts: []string{}, + Tmpfs: map[string]string{}, + Resources: containerType.Resources{ + OomKillDisable: &falseBool, + }, + NetworkMode: "b-moby-name", + }), gomock.Eq( + &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{ + "a-moby-name": { + IPAMConfig: &network.EndpointIPAMConfig{}, + Aliases: []string{"bork-test-0"}, + }, + "b-moby-name": { + IPAMConfig: &network.EndpointIPAMConfig{}, + Aliases: []string{"bork-test-0"}, + }, + }, + }), gomock.Any(), gomock.Any()).Times(1).Return( + containerType.CreateResponse{ + ID: "an-id", + }, nil) + + apiClient.EXPECT().ContainerInspect(gomock.Any(), gomock.Eq("an-id")).Times(1).Return( + moby.ContainerJSON{ + ContainerJSONBase: &moby.ContainerJSONBase{ + ID: "an-id", + Name: "a-name", + }, + Config: &containerType.Config{}, + NetworkSettings: &moby.NetworkSettings{}, + }, nil) + + _, err := tested.createMobyContainer(context.Background(), &project, service, "test", 0, nil, createOptions{ + Labels: make(types.Labels), + }, progress.ContextWriter(context.TODO())) + assert.NilError(t, err) + }) + +} diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 069db7cfc7..c034b14662 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -235,7 +235,11 @@ func (s *composeService) getCreateConfigs(ctx context.Context, if err != nil { return createConfigs{}, err } - networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases) + apiVersion, err := s.RuntimeVersion(ctx) + if err != nil { + return createConfigs{}, err + } + networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases, apiVersion) portBindings := buildContainerPortBindingOptions(service) // MISC @@ -456,6 +460,7 @@ func defaultNetworkSettings( serviceIndex int, links []string, useNetworkAliases bool, + version string, ) (container.NetworkMode, *network.NetworkingConfig) { if service.NetworkMode != "" { return container.NetworkMode(service.NetworkMode), nil @@ -465,23 +470,38 @@ func defaultNetworkSettings( return "none", nil } - var networkKey string + var primaryNetworkKey string if len(service.Networks) > 0 { - networkKey = service.NetworksByPriority()[0] + primaryNetworkKey = service.NetworksByPriority()[0] } else { - networkKey = "default" + primaryNetworkKey = "default" + } + primaryNetworkMobyNetworkName := project.Networks[primaryNetworkKey].Name + endpointsConfig := map[string]*network.EndpointSettings{ + primaryNetworkMobyNetworkName: createEndpointSettings(project, service, serviceIndex, primaryNetworkKey, links, useNetworkAliases), + } + + // Starting from API version 1.44, the Engine will take several EndpointsConfigs + // so we can pass all the extra networks we want the container to be connected to + // in the network configuration instead of connecting the container to each extra + // network individually after creation. + if versions.GreaterThanOrEqualTo(version, "1.44") && len(service.Networks) > 1 { + serviceNetworks := service.NetworksByPriority() + for _, networkKey := range serviceNetworks[1:] { + mobyNetworkName := project.Networks[networkKey].Name + epSettings := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases) + endpointsConfig[mobyNetworkName] = epSettings + } } - mobyNetworkName := project.Networks[networkKey].Name - epSettings := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases) + networkConfig := &network.NetworkingConfig{ - EndpointsConfig: map[string]*network.EndpointSettings{ - mobyNetworkName: epSettings, - }, + EndpointsConfig: endpointsConfig, } + // From the Engine API docs: // > Supported standard values are: bridge, host, none, and container:. // > Any other value is taken as a custom network's name to which this container should connect to. - return container.NetworkMode(mobyNetworkName), networkConfig + return container.NetworkMode(primaryNetworkMobyNetworkName), networkConfig } func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy { diff --git a/pkg/compose/create_test.go b/pkg/compose/create_test.go index 1a47348960..7991d09e8a 100644 --- a/pkg/compose/create_test.go +++ b/pkg/compose/create_test.go @@ -193,7 +193,7 @@ func TestDefaultNetworkSettings(t *testing.T) { }), } - networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true) + networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43") assert.Equal(t, string(networkMode), "myProject_myNetwork2") assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1)) assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork2")) @@ -221,7 +221,7 @@ func TestDefaultNetworkSettings(t *testing.T) { }), } - networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true) + networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43") assert.Equal(t, string(networkMode), "myProject_default") assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1)) assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_default")) @@ -238,7 +238,7 @@ func TestDefaultNetworkSettings(t *testing.T) { }, } - networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true) + networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43") assert.Equal(t, string(networkMode), "none") assert.Check(t, cmp.Nil(networkConfig)) }) @@ -258,7 +258,7 @@ func TestDefaultNetworkSettings(t *testing.T) { }), } - networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true) + networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43") assert.Equal(t, string(networkMode), "host") assert.Check(t, cmp.Nil(networkConfig)) }) diff --git a/pkg/compose/desktop.go b/pkg/compose/desktop.go new file mode 100644 index 0000000000..9af977fde2 --- /dev/null +++ b/pkg/compose/desktop.go @@ -0,0 +1,77 @@ +/* + Copyright 2024 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" + "os" + "strconv" + "strings" + "time" + + "github.com/docker/compose/v2/internal/desktop" + "github.com/sirupsen/logrus" +) + +// engineLabelDesktopAddress is used to detect that Compose is running with a +// Docker Desktop context. When this label is present, the value is an endpoint +// address for an in-memory socket (AF_UNIX or named pipe). +const engineLabelDesktopAddress = "com.docker.desktop.address" + +var _ desktop.IntegrationService = &composeService{} + +// MaybeEnableDesktopIntegration initializes the desktop.Client instance if +// the server info from the Docker Engine is a Docker Desktop instance. +// +// EXPERIMENTAL: Requires `COMPOSE_EXPERIMENTAL_DESKTOP=1` env var set. +func (s *composeService) MaybeEnableDesktopIntegration(ctx context.Context) error { + if desktopEnabled, _ := strconv.ParseBool(os.Getenv("COMPOSE_EXPERIMENTAL_DESKTOP")); !desktopEnabled { + return nil + } + + if s.dryRun { + return nil + } + + // safeguard to make sure this doesn't get stuck indefinitely + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + + info, err := s.dockerCli.Client().Info(ctx) + if err != nil { + return fmt.Errorf("querying server info: %w", err) + } + for _, l := range info.Labels { + k, v, ok := strings.Cut(l, "=") + if !ok || k != engineLabelDesktopAddress { + continue + } + + desktopCli := desktop.NewClient(v) + _, err := desktopCli.Ping(ctx) + if err != nil { + return fmt.Errorf("pinging Desktop API: %w", err) + } + logrus.Debugf("Enabling Docker Desktop integration (experimental): %s", v) + s.desktopCli = desktopCli + return nil + } + + logrus.Trace("Docker Desktop not detected, no integration enabled") + return nil +} diff --git a/pkg/compose/printer.go b/pkg/compose/printer.go index dea72e27a6..45b0f7914d 100644 --- a/pkg/compose/printer.go +++ b/pkg/compose/printer.go @@ -32,18 +32,19 @@ type logPrinter interface { } type printer struct { - sync.Mutex queue chan api.ContainerEvent consumer api.LogConsumer - stopped bool + stopCh chan struct{} // stopCh is a signal channel for producers to stop sending events to the queue + stop sync.Once } // newLogPrinter builds a LogPrinter passing containers logs to LogConsumer func newLogPrinter(consumer api.LogConsumer) logPrinter { - queue := make(chan api.ContainerEvent) printer := printer{ consumer: consumer, - queue: queue, + queue: make(chan api.ContainerEvent), + stopCh: make(chan struct{}), + stop: sync.Once{}, } return &printer } @@ -54,24 +55,27 @@ func (p *printer) Cancel() { } func (p *printer) Stop() { - p.Lock() - defer p.Unlock() - if !p.stopped { - // only close if this is the first call to stop - p.stopped = true - close(p.queue) - } + p.stop.Do(func() { + close(p.stopCh) + for { + select { + case <-p.queue: + // purge the queue to free producers goroutines + // p.queue will be garbage collected + default: + return + } + } + }) } func (p *printer) HandleEvent(event api.ContainerEvent) { - p.Lock() - defer p.Unlock() - if p.stopped { - // prevent deadlocking, if the printer is done, there's no reader for - // queue, so this write could block indefinitely + select { + case <-p.stopCh: return + default: + p.queue <- event } - p.queue <- event } //nolint:gocyclo @@ -80,58 +84,67 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error aborting bool exitCode int ) - containers := map[string]struct{}{} - for event := range p.queue { - container, id := event.Container, event.ID - switch event.Type { - case api.UserCancel: - aborting = true - case api.ContainerEventAttach: - if _, ok := containers[id]; ok { - continue - } - containers[id] = struct{}{} - p.consumer.Register(container) - case api.ContainerEventExit, api.ContainerEventStopped, api.ContainerEventRecreated: - if !event.Restarting { - delete(containers, id) - } - if !aborting { - p.consumer.Status(container, fmt.Sprintf("exited with code %d", event.ExitCode)) - if event.Type == api.ContainerEventRecreated { - p.consumer.Status(container, "has been recreated") + defer p.Stop() + + // containers we are tracking. Use true when container is running, false after we receive a stop|die signal + containers := map[string]bool{} + for { + select { + case <-p.stopCh: + return exitCode, nil + case event := <-p.queue: + container, id := event.Container, event.ID + switch event.Type { + case api.UserCancel: + aborting = true + case api.ContainerEventAttach: + if _, ok := containers[id]; ok { + continue } - } - if cascadeStop { - if !aborting { - aborting = true - err := stopFn() - if err != nil { - return 0, err + containers[id] = true + p.consumer.Register(container) + case api.ContainerEventExit, api.ContainerEventStopped, api.ContainerEventRecreated: + if !aborting && containers[id] { + p.consumer.Status(container, fmt.Sprintf("exited with code %d", event.ExitCode)) + if event.Type == api.ContainerEventRecreated { + p.consumer.Status(container, "has been recreated") } } - if event.Type == api.ContainerEventExit { - if exitCodeFrom == "" { - exitCodeFrom = event.Service + containers[id] = false + if !event.Restarting { + delete(containers, id) + } + + if cascadeStop { + if !aborting { + aborting = true + err := stopFn() + if err != nil { + return 0, err + } } - if exitCodeFrom == event.Service { - exitCode = event.ExitCode + if event.Type == api.ContainerEventExit { + if exitCodeFrom == "" { + exitCodeFrom = event.Service + } + if exitCodeFrom == event.Service { + exitCode = event.ExitCode + } } } - } - if len(containers) == 0 { - // Last container terminated, done - return exitCode, nil - } - case api.ContainerEventLog: - if !aborting { - p.consumer.Log(container, event.Line) - } - case api.ContainerEventErr: - if !aborting { - p.consumer.Err(container, event.Line) + if len(containers) == 0 { + // Last container terminated, done + return exitCode, nil + } + case api.ContainerEventLog: + if !aborting { + p.consumer.Log(container, event.Line) + } + case api.ContainerEventErr: + if !aborting { + p.consumer.Err(container, event.Line) + } } } } - return exitCode, nil } diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 406da5ec2b..a01d7be584 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -26,7 +26,6 @@ import ( "github.com/docker/compose/v2/internal/ocipush" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" - "github.com/opencontainers/go-digest" ) func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error { @@ -111,17 +110,7 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje if err != nil { return nil, err } - project, err = project.WithImagesResolved(func(named reference.Named) (digest.Digest, error) { - auth, err := encodedAuth(named, s.configFile()) - if err != nil { - return "", err - } - inspect, err := s.apiClient().DistributionInspect(ctx, named.String(), auth) - if err != nil { - return "", err - } - return inspect.Descriptor.Digest, nil - }) + project, err = project.WithImagesResolved(ImageDigestResolver(ctx, s.configFile(), s.apiClient())) if err != nil { return nil, err } diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index 977397bd09..222876d657 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -28,10 +28,13 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/distribution/reference" "github.com/docker/buildx/driver" + "github.com/docker/cli/cli/config/configfile" moby "github.com/docker/docker/api/types" + "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" "github.com/hashicorp/go-multierror" + "github.com/opencontainers/go-digest" "golang.org/x/sync/errgroup" "github.com/docker/compose/v2/pkg/api" @@ -242,6 +245,21 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser return inspected.ID, nil } +// ImageDigestResolver creates a func able to resolve image digest from a docker ref, +func ImageDigestResolver(ctx context.Context, file *configfile.ConfigFile, apiClient client.APIClient) func(named reference.Named) (digest.Digest, error) { + return func(named reference.Named) (digest.Digest, error) { + auth, err := encodedAuth(named, file) + if err != nil { + return "", err + } + inspect, err := apiClient.DistributionInspect(ctx, named.String(), auth) + if err != nil { + return "", err + } + return inspect.Descriptor.Digest, nil + } +} + func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) { repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { diff --git a/pkg/compose/scale.go b/pkg/compose/scale.go index 1e869ea726..985cc129d1 100644 --- a/pkg/compose/scale.go +++ b/pkg/compose/scale.go @@ -25,7 +25,7 @@ import ( ) func (s *composeService) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error { - return progress.Run(ctx, tracing.SpanWrapFunc("project/scale", tracing.ProjectOptions(project), func(ctx context.Context) error { + return progress.Run(ctx, tracing.SpanWrapFunc("project/scale", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error { err := s.create(ctx, project, api.CreateOptions{Services: options.Services}) if err != nil { return err diff --git a/pkg/compose/up.go b/pkg/compose/up.go index be6bbdb149..ab22de48b7 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -32,7 +32,7 @@ import ( ) func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo - err := progress.Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(project), func(ctx context.Context) error { + err := progress.Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error { w := progress.ContextWriter(ctx) w.HasMore(options.Start.Attach == nil) err := s.create(ctx, project, options.Create) @@ -125,6 +125,17 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options return err }) + if options.Start.Watch { + eg.Go(func() error { + buildOpts := *options.Create.Build + buildOpts.Quiet = true + return s.Watch(ctx, project, options.Start.Services, api.WatchOptions{ + Build: &buildOpts, + LogTo: options.Start.Attach, + }) + }) + } + // We don't use parent (cancelable) context as we manage sigterm to stop the stack err = s.start(context.Background(), project.Name, options.Start, printer.HandleEvent) if err != nil && !isTerminated { // Ignore error if the process is terminated diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 3673c43557..2a651baac1 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -46,21 +46,23 @@ type fileEvent struct { Action types.WatchAction } -// getSyncImplementation returns the the tar-based syncer unless it has been explicitly -// disabled with `COMPOSE_EXPERIMENTAL_WATCH_TAR=0`. Note that the absence of the env -// var means enabled. -func (s *composeService) getSyncImplementation(project *types.Project) sync.Syncer { +// getSyncImplementation returns an appropriate sync implementation for the +// project. +// +// Currently, an implementation that batches files and transfers them using +// the Moby `Untar` API. +func (s *composeService) getSyncImplementation(project *types.Project) (sync.Syncer, error) { var useTar bool if useTarEnv, ok := os.LookupEnv("COMPOSE_EXPERIMENTAL_WATCH_TAR"); ok { useTar, _ = strconv.ParseBool(useTarEnv) } else { useTar = true } - if useTar { - return sync.NewTar(project.Name, tarDockerClient{s: s}) + if !useTar { + return nil, errors.New("no available sync implementation") } - return sync.NewDockerCopy(project.Name, s, s.stdinfo()) + return sync.NewTar(project.Name, tarDockerClient{s: s}), nil } func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo @@ -68,9 +70,13 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv if project, err = project.WithSelectedServices(services); err != nil { return err } - syncer := s.getSyncImplementation(project) + syncer, err := s.getSyncImplementation(project) + if err != nil { + return err + } eg, ctx := errgroup.WithContext(ctx) watching := false + options.LogTo.Register(api.WatchLogger) for i := range project.Services { service := project.Services[i] config, err := loadDevelopmentConfig(service, project) @@ -86,9 +92,15 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv continue } - if len(config.Watch) > 0 && service.Build == nil { - // service configured with watchers but no build section - return fmt.Errorf("can't watch service %q without a build context", service.Name) + for _, trigger := range config.Watch { + if trigger.Action == types.WatchActionRebuild { + if service.Build == nil { + return fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild) + } + if options.Build == nil { + return fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name) + } + } } if len(services) > 0 && service.Build == nil { @@ -137,9 +149,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv return err } - fmt.Fprintf( - s.stdinfo(), - "Watch configuration for service %q:%s\n", + logrus.Debugf("Watch configuration for service %q:%s\n", service.Name, strings.Join(append([]string{""}, pathLogs...), "\n - "), ) @@ -158,6 +168,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv if !watching { return fmt.Errorf("none of the selected services is configured for watch, consider setting an 'develop' section") } + options.LogTo.Log(api.WatchLogger, "watch enabled") return eg.Wait() } @@ -185,7 +196,7 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, name case batch := <-batchEvents: start := time.Now() logrus.Debugf("batch start: service[%s] count[%d]", name, len(batch)) - if err := s.handleWatchBatch(ctx, project, name, options.Build, batch, syncer); err != nil { + if err := s.handleWatchBatch(ctx, project, name, options, batch, syncer); err != nil { logrus.Warnf("Error handling changed files for service %s: %v", name, err) } logrus.Debugf("batch complete: service[%s] duration[%s] count[%d]", @@ -426,32 +437,37 @@ func (t tarDockerClient) Untar(ctx context.Context, id string, archive io.ReadCl }) } -func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Project, serviceName string, build api.BuildOptions, batch []fileEvent, syncer sync.Syncer) error { +func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Project, serviceName string, options api.WatchOptions, batch []fileEvent, syncer sync.Syncer) error { pathMappings := make([]sync.PathMapping, len(batch)) restartService := false for i := range batch { if batch[i].Action == types.WatchActionRebuild { - fmt.Fprintf( - s.stdinfo(), - "Rebuilding service %q after changes were detected:%s\n", - serviceName, - strings.Join(append([]string{""}, batch[i].HostPath), "\n - "), - ) + options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service %q after changes were detected...", serviceName)) // restrict the build to ONLY this service, not any of its dependencies - build.Services = []string{serviceName} - err := s.Up(ctx, project, api.UpOptions{ - Create: api.CreateOptions{ - Build: &build, - Services: []string{serviceName}, - Inherit: true, - }, - Start: api.StartOptions{ - Services: []string{serviceName}, - Project: project, - }, + options.Build.Services = []string{serviceName} + _, err := s.build(ctx, project, *options.Build, nil) + if err != nil { + options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Build failed. Error: %v", err)) + return err + } + options.LogTo.Log(api.WatchLogger, fmt.Sprintf("service %q successfully built", serviceName)) + + err = s.create(ctx, project, api.CreateOptions{ + Services: []string{serviceName}, + Inherit: true, + Recreate: api.RecreateForce, }) if err != nil { - fmt.Fprintf(s.stderr(), "Application failed to start after update. Error: %v\n", err) + options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Failed to recreate service after update. Error: %v", err)) + return err + } + + err = s.start(ctx, project.Name, api.StartOptions{ + Project: project, + Services: []string{serviceName}, + }, nil) + if err != nil { + options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Application failed to start after update. Error: %v", err)) } return nil } @@ -461,7 +477,7 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr pathMappings[i] = batch[i].PathMapping } - writeWatchSyncMessage(s.stdinfo(), serviceName, pathMappings) + writeWatchSyncMessage(options.LogTo, serviceName, pathMappings) service, err := project.GetService(serviceName) if err != nil { @@ -481,29 +497,19 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr } // writeWatchSyncMessage prints out a message about the sync for the changed paths. -func writeWatchSyncMessage(w io.Writer, serviceName string, pathMappings []sync.PathMapping) { +func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings []sync.PathMapping) { const maxPathsToShow = 10 if len(pathMappings) <= maxPathsToShow || logrus.IsLevelEnabled(logrus.DebugLevel) { hostPathsToSync := make([]string, len(pathMappings)) for i := range pathMappings { hostPathsToSync[i] = pathMappings[i].HostPath } - fmt.Fprintf( - w, - "Syncing %q after changes were detected:%s\n", - serviceName, - strings.Join(append([]string{""}, hostPathsToSync...), "\n - "), - ) + log.Log(api.WatchLogger, fmt.Sprintf("Syncing %q after changes were detected", serviceName)) } else { hostPathsToSync := make([]string, len(pathMappings)) for i := range pathMappings { hostPathsToSync[i] = pathMappings[i].HostPath } - fmt.Fprintf( - w, - "Syncing service %q after %d changes were detected\n", - serviceName, - len(pathMappings), - ) + log.Log(api.WatchLogger, fmt.Sprintf("Syncing service %q after %d changes were detected", serviceName, len(pathMappings))) } } diff --git a/pkg/compose/watch_test.go b/pkg/compose/watch_test.go index fc39ee7d34..39fbf2bcb2 100644 --- a/pkg/compose/watch_test.go +++ b/pkg/compose/watch_test.go @@ -16,6 +16,7 @@ package compose import ( "context" + "fmt" "os" "testing" "time" @@ -91,10 +92,29 @@ func (t testWatcher) Errors() chan error { return t.errors } +type stdLogger struct{} + +func (s stdLogger) Log(containerName, message string) { + fmt.Printf("%s: %s\n", containerName, message) +} + +func (s stdLogger) Err(containerName, message string) { + fmt.Fprintf(os.Stderr, "%s: %s\n", containerName, message) +} + +func (s stdLogger) Status(container, msg string) { + fmt.Printf("%s: %s\n", container, msg) +} + +func (s stdLogger) Register(container string) { + +} + func TestWatch_Sync(t *testing.T) { mockCtrl := gomock.NewController(t) cli := mocks.NewMockCli(mockCtrl) cli.EXPECT().Err().Return(os.Stderr).AnyTimes() + cli.EXPECT().BuildKitEnabled().Return(true, nil) apiClient := mocks.NewMockAPIClient(mockCtrl) apiClient.EXPECT().ContainerList(gomock.Any(), gomock.Any()).Return([]moby.Container{ testContainer("test", "123", false), @@ -124,7 +144,10 @@ func TestWatch_Sync(t *testing.T) { dockerCli: cli, clock: clock, } - err := service.watch(ctx, &proj, "test", api.WatchOptions{}, watcher, syncer, []types.Trigger{ + err := service.watch(ctx, &proj, "test", api.WatchOptions{ + Build: &api.BuildOptions{}, + LogTo: stdLogger{}, + }, watcher, syncer, []types.Trigger{ { Path: "/sync", Action: "sync", diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index 349ab9f4cb..65044519b5 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -306,7 +306,7 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) { "-f", "fixtures/build-test/platforms/compose-unsupported-platform.yml", "build") res.Assert(t, icmd.Expected{ ExitCode: 17, - Err: "failed to solve: alpine: no match for platform in", + Err: "no match for platform in", }) }) diff --git a/pkg/e2e/cancel_test.go b/pkg/e2e/cancel_test.go index 6e34bd8d2d..a3f4b77cda 100644 --- a/pkg/e2e/cancel_test.go +++ b/pkg/e2e/cancel_test.go @@ -37,18 +37,26 @@ func TestComposeCancel(t *testing.T) { c := NewParallelCLI(t) t.Run("metrics on cancel Compose build", func(t *testing.T) { - c.RunDockerComposeCmd(t, "ls") - buildProjectPath := "fixtures/build-infinite/compose.yaml" + const buildProjectPath = "fixtures/build-infinite/compose.yaml" + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // require a separate groupID from the process running tests, in order to simulate ctrl+C from a terminal. // sending kill signal - stdout := &utils.SafeBuffer{} - stderr := &utils.SafeBuffer{} - cmd, err := StartWithNewGroupID(context.Background(), + var stdout, stderr utils.SafeBuffer + cmd, err := StartWithNewGroupID( + ctx, c.NewDockerComposeCmd(t, "-f", buildProjectPath, "build", "--progress", "plain"), - stdout, - stderr) + &stdout, + &stderr, + ) assert.NilError(t, err) + processDone := make(chan error, 1) + go func() { + defer close(processDone) + processDone <- cmd.Wait() + }() c.WaitForCondition(t, func() (bool, string) { out := stdout.String() @@ -58,15 +66,21 @@ func TestComposeCancel(t *testing.T) { errors) }, 30*time.Second, 1*time.Second) - err = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) // simulate Ctrl-C : send signal to processGroup, children will have same groupId by default + // simulate Ctrl-C : send signal to processGroup, children will have same groupId by default + err = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) assert.NilError(t, err) - c.WaitForCondition(t, func() (bool, string) { - out := stdout.String() - errors := stderr.String() - return strings.Contains(out, "CANCELED"), fmt.Sprintf("'CANCELED' not found in : \n%s\nStderr: \n%s\n", out, - errors) - }, 10*time.Second, 1*time.Second) + select { + case <-ctx.Done(): + t.Fatal("test context canceled") + case err := <-processDone: + // TODO(milas): Compose should really not return exit code 130 here, + // this is an old hack for the compose-cli wrapper + assert.Error(t, err, "exit status 130", + "STDOUT:\n%s\nSTDERR:\n%s\n", stdout.String(), stderr.String()) + case <-time.After(10 * time.Second): + t.Fatal("timeout waiting for Compose exit") + } }) } diff --git a/pkg/e2e/compose_run_test.go b/pkg/e2e/compose_run_test.go index b074e6a80e..ceec152a86 100644 --- a/pkg/e2e/compose_run_test.go +++ b/pkg/e2e/compose_run_test.go @@ -160,4 +160,13 @@ func TestLocalComposeRun(t *testing.T) { c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "down", "--remove-orphans") }) + + t.Run("--quiet-pull", func(t *testing.T) { + res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "down", "--rmi", "all") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "run", "--quiet-pull", "backend") + assert.Assert(t, !strings.Contains(res.Combined(), "Pull complete"), res.Combined()) + assert.Assert(t, strings.Contains(res.Combined(), "Pulled"), res.Combined()) + }) } diff --git a/pkg/e2e/compose_test.go b/pkg/e2e/compose_test.go index 6384c29c86..75d546f730 100644 --- a/pkg/e2e/compose_test.go +++ b/pkg/e2e/compose_test.go @@ -235,7 +235,7 @@ func TestCompatibility(t *testing.T) { }) } -func TestConvert(t *testing.T) { +func TestConfig(t *testing.T) { const projectName = "compose-e2e-convert" c := NewParallelCLI(t) @@ -244,7 +244,8 @@ func TestConvert(t *testing.T) { t.Run("up", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose.yaml", "-p", projectName, "convert") - res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`services: + res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s +services: nginx: build: context: %s @@ -253,11 +254,12 @@ func TestConvert(t *testing.T) { default: null networks: default: - name: compose-e2e-convert_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) + name: compose-e2e-convert_default +`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) }) } -func TestConvertInterpolate(t *testing.T) { +func TestConfigInterpolate(t *testing.T) { const projectName = "compose-e2e-convert-interpolate" c := NewParallelCLI(t) @@ -266,16 +268,18 @@ func TestConvertInterpolate(t *testing.T) { t.Run("convert", func(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "convert", "--no-interpolate") - res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`services: + res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s +networks: + default: + name: compose-e2e-convert-interpolate_default +services: nginx: build: context: %s dockerfile: ${MYVAR} networks: default: null -networks: - default: - name: compose-e2e-convert-interpolate_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) +`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) }) } @@ -313,3 +317,15 @@ func TestRemoveOrphaned(t *testing.T) { res := c.RunDockerComposeCmd(t, "-f", "./fixtures/sentences/compose.yaml", "-p", projectName, "ps", "--format", "{{.Name}}") res.Assert(t, icmd.Expected{Out: fmt.Sprintf("%s-words-1", projectName)}) } + +func TestResolveDotEnv(t *testing.T) { + c := NewCLI(t) + + cmd := c.NewDockerComposeCmd(t, "config") + cmd.Dir = filepath.Join(".", "fixtures", "dotenv") + res := icmd.RunCmd(cmd) + res.Assert(t, icmd.Expected{ + ExitCode: 0, + Out: "image: backend:latest", + }) +} diff --git a/pkg/e2e/e2e_config_plugin.go b/pkg/e2e/e2e_config_plugin.go index 5de99481f1..84ca6eabcd 100644 --- a/pkg/e2e/e2e_config_plugin.go +++ b/pkg/e2e/e2e_config_plugin.go @@ -18,4 +18,4 @@ package e2e -const composeStandaloneMode = true +const composeStandaloneMode = false diff --git a/pkg/e2e/fixtures/dotenv/.env b/pkg/e2e/fixtures/dotenv/.env new file mode 100644 index 0000000000..869938aa7c --- /dev/null +++ b/pkg/e2e/fixtures/dotenv/.env @@ -0,0 +1 @@ +COMPOSE_FILE="${COMPOSE_FILE:-development/compose.yaml}" \ No newline at end of file diff --git a/pkg/e2e/fixtures/dotenv/development/.env b/pkg/e2e/fixtures/dotenv/development/.env new file mode 100644 index 0000000000..9369028779 --- /dev/null +++ b/pkg/e2e/fixtures/dotenv/development/.env @@ -0,0 +1,2 @@ +IMAGE_NAME="${IMAGE_NAME:-backend}" +IMAGE_TAG="${IMAGE_TAG:-latest}" diff --git a/pkg/e2e/fixtures/dotenv/development/compose.yaml b/pkg/e2e/fixtures/dotenv/development/compose.yaml new file mode 100644 index 0000000000..b44805e305 --- /dev/null +++ b/pkg/e2e/fixtures/dotenv/development/compose.yaml @@ -0,0 +1,3 @@ +services: + backend: + image: $IMAGE_NAME:$IMAGE_TAG diff --git a/pkg/e2e/fixtures/logs-test/cat.yaml b/pkg/e2e/fixtures/logs-test/cat.yaml new file mode 100644 index 0000000000..76bd5a9ab6 --- /dev/null +++ b/pkg/e2e/fixtures/logs-test/cat.yaml @@ -0,0 +1,6 @@ +services: + test: + image: alpine + command: cat /text_file.txt + volumes: + - ${FILE}:/text_file.txt diff --git a/pkg/e2e/fixtures/run-test/quiet-pull.yaml b/pkg/e2e/fixtures/run-test/quiet-pull.yaml new file mode 100644 index 0000000000..922676363f --- /dev/null +++ b/pkg/e2e/fixtures/run-test/quiet-pull.yaml @@ -0,0 +1,3 @@ +services: + backend: + image: hello-world \ No newline at end of file diff --git a/pkg/e2e/fixtures/scale/compose.yaml b/pkg/e2e/fixtures/scale/compose.yaml index 9ff67af699..619630876b 100644 --- a/pkg/e2e/fixtures/scale/compose.yaml +++ b/pkg/e2e/fixtures/scale/compose.yaml @@ -5,6 +5,8 @@ services: - db db: image: nginx:alpine + environment: + - MAYBE front: image: nginx:alpine deploy: diff --git a/pkg/e2e/logs_test.go b/pkg/e2e/logs_test.go index d22347b617..ff4cbab92d 100644 --- a/pkg/e2e/logs_test.go +++ b/pkg/e2e/logs_test.go @@ -17,6 +17,10 @@ package e2e import ( + "fmt" + "io" + "os" + "path/filepath" "strings" "testing" "time" @@ -96,6 +100,28 @@ func TestLocalComposeLogsFollow(t *testing.T) { poll.WaitOn(t, expectOutput(res, "ping-2 "), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(20*time.Second)) } +func TestLocalComposeLargeLogs(t *testing.T) { + const projectName = "compose-e2e-large_logs" + file := filepath.Join(t.TempDir(), "large.txt") + c := NewCLI(t, WithEnv("FILE="+file)) + t.Cleanup(func() { + c.RunDockerComposeCmd(t, "--project-name", projectName, "down") + }) + + f, err := os.Create(file) + assert.NilError(t, err) + for i := 0; i < 300_000; i++ { + _, err := io.WriteString(f, fmt.Sprintf("This is line %d in a laaaarge text file\n", i)) + assert.NilError(t, err) + } + assert.NilError(t, f.Close()) + + cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/logs-test/cat.yaml", "--project-name", projectName, "up", "--abort-on-container-exit") + cmd.Stdout = io.Discard + res := icmd.RunCmd(cmd) + res.Assert(t, icmd.Expected{Out: "test-1 exited with code 0"}) +} + func expectOutput(res *icmd.Result, expected string) func(t poll.LogT) poll.Result { return func(t poll.LogT) poll.Result { if strings.Contains(res.Stdout(), expected) { diff --git a/pkg/e2e/scale_test.go b/pkg/e2e/scale_test.go index 1cd80ad1d3..21595dd477 100644 --- a/pkg/e2e/scale_test.go +++ b/pkg/e2e/scale_test.go @@ -95,6 +95,78 @@ func TestScaleWithDepsCases(t *testing.T) { checkServiceContainer(t, res.Combined(), "scale-deps-tests-db", NO_STATE_TO_CHECK, 1) } +func TestScaleUpAndDownPreserveContainerNumber(t *testing.T) { + const projectName = "scale-up-down-test" + + c := NewCLI(t, WithEnv( + "COMPOSE_PROJECT_NAME="+projectName)) + + reset := func() { + c.RunDockerComposeCmd(t, "down", "--rmi", "all") + } + t.Cleanup(reset) + res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2") + + t.Log("scale down removes replica #2") + res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=1", "db") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1") + + t.Log("scale up restores replica #2") + res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2") +} + +func TestScaleDownRemovesObsolete(t *testing.T) { + const projectName = "scale-down-obsolete-test" + c := NewCLI(t, WithEnv( + "COMPOSE_PROJECT_NAME="+projectName)) + + reset := func() { + c.RunDockerComposeCmd(t, "down", "--rmi", "all") + } + t.Cleanup(reset) + res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "db") + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1") + + cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=2", "db") + res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) { + cmd.Env = append(cmd.Env, "MAYBE=value") + }) + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1\n"+projectName+"-db-2") + + t.Log("scale down removes obsolete replica #1") + cmd = c.NewDockerComposeCmd(t, "--project-directory", "fixtures/scale", "up", "-d", "--scale", "db=1", "db") + res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) { + cmd.Env = append(cmd.Env, "MAYBE=value") + }) + res.Assert(t, icmd.Success) + + res = c.RunDockerComposeCmd(t, "ps", "--format", "{{.Name}}", "db") + res.Assert(t, icmd.Success) + assert.Equal(t, strings.TrimSpace(res.Stdout()), projectName+"-db-1") +} + func checkServiceContainer(t *testing.T, stdout, containerName, containerState string, count int) { found := 0 lines := strings.Split(stdout, "\n") diff --git a/pkg/e2e/watch_test.go b/pkg/e2e/watch_test.go index 0740e98c22..677b080a0a 100644 --- a/pkg/e2e/watch_test.go +++ b/pkg/e2e/watch_test.go @@ -21,7 +21,6 @@ import ( "fmt" "os" "path/filepath" - "strconv" "strings" "sync/atomic" "testing" @@ -38,23 +37,12 @@ func TestWatch(t *testing.T) { t.Skip("Skipping watch tests until we can figure out why they are flaky/failing") services := []string{"alpine", "busybox", "debian"} - t.Run("docker cp", func(t *testing.T) { - for _, svcName := range services { - t.Run(svcName, func(t *testing.T) { - t.Helper() - doTest(t, svcName, false) - }) - } - }) - - t.Run("tar", func(t *testing.T) { - for _, svcName := range services { - t.Run(svcName, func(t *testing.T) { - t.Helper() - doTest(t, svcName, true) - }) - } - }) + for _, svcName := range services { + t.Run(svcName, func(t *testing.T) { + t.Helper() + doTest(t, svcName) + }) + } } func TestRebuildOnDotEnvWithExternalNetwork(t *testing.T) { @@ -150,8 +138,9 @@ func TestRebuildOnDotEnvWithExternalNetwork(t *testing.T) { } -// NOTE: these tests all share a single Compose file but are safe to run concurrently -func doTest(t *testing.T, svcName string, tarSync bool) { +// NOTE: these tests all share a single Compose file but are safe to run +// concurrently (though that's not recommended). +func doTest(t *testing.T, svcName string) { tmpdir := t.TempDir() dataDir := filepath.Join(tmpdir, "data") configDir := filepath.Join(tmpdir, "config") @@ -171,13 +160,9 @@ func doTest(t *testing.T, svcName string, tarSync bool) { CopyFile(t, filepath.Join("fixtures", "watch", "compose.yaml"), composeFilePath) projName := "e2e-watch-" + svcName - if tarSync { - projName += "-tar" - } env := []string{ "COMPOSE_FILE=" + composeFilePath, "COMPOSE_PROJECT_NAME=" + projName, - "COMPOSE_EXPERIMENTAL_WATCH_TAR=" + strconv.FormatBool(tarSync), } cli := NewCLI(t, WithEnv(env...))