Compare commits

..

57 Commits

Author SHA1 Message Date
Guillaume Lours
a8bfbc147a bump compose-go v2.4.4
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-11-07 14:19:53 +01:00
Joana Hrotko
fbbd6f83d7 Avoid starting all services on rebuild
Signed-off-by: Joana Hrotko <joana.hrotko@docker.com>
2024-11-05 14:51:12 +00:00
Guillaume Lours
a000978980 remove ArtifactType from Config in OCI v1.1 definition of the artifact
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-11-05 15:10:56 +01:00
dependabot[bot]
361c0893a9 build(deps): bump github.com/compose-spec/compose-go/v2
Bumps [github.com/compose-spec/compose-go/v2](https://github.com/compose-spec/compose-go) from 2.4.2 to 2.4.3.
- [Release notes](https://github.com/compose-spec/compose-go/releases)
- [Commits](https://github.com/compose-spec/compose-go/compare/v2.4.2...v2.4.3)

---
updated-dependencies:
- dependency-name: github.com/compose-spec/compose-go/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 10:50:25 +01:00
Nicolas De Loof
513b6128c2 Service being declared in a profile must not trigger re-creation
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-11-05 10:49:08 +01:00
Joana Hrotko
eececb9add Add profile e2e test case to document in compose
Signed-off-by: Joana Hrotko <joana.hrotko@docker.com>
2024-10-30 16:00:41 +01:00
Laura Brehm
501b5acde6 Update MAINTAINERS file
Add `jhrotko` to Core Maintainers.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-10-30 13:33:46 +01:00
Guillaume Lours
f51bc4cd00 bump compose-go to version v2.4.2
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-30 11:30:53 +01:00
Guillaume Lours
517f87a372 bump google.golang.org/grpc to v1.67.1
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-29 12:07:34 +01:00
Guillaume Lours
718049cbd7 bump go.uber.org/mock to v0.5.0
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-29 12:07:34 +01:00
Guillaume Lours
02371f3127 bump golang minimal version to 1.22 in go.mod
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-29 11:45:02 +01:00
dependabot[bot]
a7c9de82b2 build(deps): bump github.com/containerd/containerd from 1.7.22 to 1.7.23
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.22 to 1.7.23.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.22...v1.7.23)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-29 09:47:38 +01:00
Guillaume Lours
51ebeb5441 introduce generate command as alpha command
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-28 17:00:59 +01:00
Guillaume Lours
fafaa9c5b8 bump compose-go to version v2.4.1
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-28 14:24:07 +01:00
Suleiman Dibirov
fc9c3cde06 Add license header to dockerignore_test.go
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2024-10-28 12:26:45 +01:00
Suleiman Dibirov
73bfbab54b fix
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2024-10-28 12:26:45 +01:00
Suleiman Dibirov
2ac081b4c4 fix(dockerignore): Add wildcard support to dockerignore.go
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2024-10-28 12:26:45 +01:00
Chris Crone
eeea049f17 push: Fix error message typo
Signed-off-by: Chris Crone <christopher.crone@docker.com>
2024-10-28 10:08:44 +01:00
Guillaume Lours
26064d4b60 allow usage of -f flag with oci Compose artifact
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-25 17:36:04 +02:00
Nicolas De Loof
7c46beb8af resurrect --all flag for cp to target oneoff container
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-25 16:59:19 +02:00
Nicolas De Loof
aa1ec4524c connect to external networks by name
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-25 16:42:28 +02:00
Nicolas De Loof
a4ee6ca7a5 don't warn about uid/gid not being supported while ... they are
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-25 12:48:00 +02:00
Guillaume Lours
5617eff0c2 bump compose-go to v2.4.0
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-24 14:37:42 +02:00
Nicolas De Loof
fa24ab8e25 one-off container are not indexed, and must be ignored by exec --index command
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-22 15:29:12 +02:00
Nicolas De Loof
0aad9595a5 don't use progress to render restart, which hides logs
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-22 15:28:55 +02:00
Nicolas De Loof
813900180e compose-go clean volume target to avoid ambiguous comparisons
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-22 11:40:35 +02:00
Nicolas De Loof
82417bd5bc add support for bind.recursive
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-16 22:19:58 +02:00
divinity76
0cbb73c024 Improve error message to include expected network label
Updated the error message when a network is found with an incorrect label to also display the expected label value. This provides more context for debugging.

Signed-off-by: divinity76 <hans@loltek.net>
2024-10-15 15:33:16 +02:00
David Scott
38e3d670a9 desktop: allow this client to be identified via user-agent
Previously the HTTP requests were sent with a generic Go-http-client
user-agent which made it hard to determine where the requests are
coming from. It's important that we can find clients so that they
can be updated if APIs change in future.

Signed-off-by: David Scott <dave@recoil.org>
2024-10-15 13:59:49 +02:00
Guillaume Lours
24c78728e0 bump compose-go to v2.3.0
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-10-10 11:18:25 +02:00
Nicolas De Loof
9eeb2d3154 convert gpus to DeviceRequests with implicit "gpu" capability
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-09 15:18:49 +02:00
Sebastiaan van Stijn
8da82c98ef gha: set default permissions to "contents: read"
make the OpenSSF scorecard slightly happier;
https://securityscorecards.dev/viewer/?uri=github.com/docker/compose

    Warn: jobLevel 'contents' permission set to 'write': .github/workflows/ci.yml:256: update your workflow using https://app.stepsecurity.io/secureworkflow/docker/compose/ci.yml/main?enable=permissions
    Warn: no topLevel permission defined: .github/workflows/docs-upstream.yml:1: update your workflow using https://app.stepsecurity.io/secureworkflow/docker/compose/docs-upstream.yml/main?enable=permissions

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-09 15:02:57 +02:00
Sebastiaan van Stijn
1a8c855489 Add security policy
Add a security policy to inform users where to report security issues,
and to make the OpenSSF scorecard slightly happier;
https://securityscorecards.dev/viewer/?uri=github.com/docker/compose

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-09 11:05:29 +02:00
Nicolas De Loof
15bd0b0c5f add support for raw env_file format
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-08 18:33:54 +02:00
dependabot[bot]
39d0f6477e build(deps): bump golang.org/x/sys from 0.25.0 to 0.26.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/sys/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-08 10:42:02 +01:00
Nicolas De Loof
3a95a0872d add support for CDI device request using devices
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-08 11:27:22 +02:00
Nicolas De Loof
f794c79eb3 Support Dockerfile-specific ignore-file with watch
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-08 11:19:02 +02:00
Joana Hrotko
407d825705 Remove feature flag integration with Docker Desktop for ComposeUI and ComposeNav
Signed-off-by: Joana Hrotko <joana.hrotko@docker.com>
2024-10-07 20:08:12 +02:00
Nicolas De Loof
82b41b9ebd introduce service hooks
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-07 16:06:41 +02:00
Nicolas De Loof
6c06170eb0 pass device.options to engine
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-04 14:55:53 +02:00
MohammadHasan Akbari
60c1311f67 chore: remove errors depricated pkg
Signed-off-by: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com>
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
2024-10-02 21:15:59 +02:00
MohammadHasan Akbari
17add87e4e fix: validate-go-mod
Signed-off-by: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com>
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
2024-10-02 21:15:59 +02:00
MohammadHasan Akbari
bf0418bac1 fix: lint
Signed-off-by: MohammadHasan Akbari <116190942+jarqvi@users.noreply.github.com>
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
2024-10-02 21:15:59 +02:00
MohammadHasan Akbari
b9d0c77cde feat: add export command
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
2024-10-02 21:15:59 +02:00
Ananta Dwi Prasetya Purna Yuda
bdb8545611 fix(convergence): Serialize access to observed state
Signed-off-by: Ananta Dwi Prasetya Purna Yuda <hi@anantadwi13.com>
2024-10-02 15:04:38 +02:00
Joana Hrotko
41df35c1f4 Remove bind options when creating a volume type
Signed-off-by: Joana Hrotko <joana.hrotko@docker.com>
2024-10-02 10:00:28 +02:00
Nicolas De Loof
3ef5045a08 Bump docker v27.3.1
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-10-02 08:32:14 +02:00
Suleiman Dibirov
d9df7aab6e fix(push): Fix unexpected EOF on alpha publish
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2024-09-30 10:38:18 +02:00
Guillaume Lours
c9d96b449b use compose-go version fixing extra_hosts unicity issue
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-09-26 17:08:18 +01:00
Joana Hrotko
1744b45765 Show watch error message and open DD only when w is pressed
Signed-off-by: Joana Hrotko <joana.hrotko@docker.com>
2024-09-26 10:19:34 +01:00
Suleiman Dibirov
87f457e7dc add tests to down.go
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2024-09-26 10:36:37 +02:00
Suleiman Dibirov
abcc91e2b9 fix(down): Fix down command if specified services are not running
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2024-09-26 10:36:37 +02:00
Joana Hrotko
8b9fe89842 After container restart register printer consumer
Signed-off-by: Joana Hrotko <joana.hrotko@docker.com>
2024-09-24 14:01:51 +02:00
Nicolas De Loof
34b18194f7 check secret source exists, as bind mount would create target as a folder otherwise
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-09-23 19:36:41 +02:00
Nicolas De Loof
ce27dba52e wait for dependent service up to delay set by --wait-timeout
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-09-23 18:25:02 +01:00
Nicolas De Loof
d2b9456137 append unix-style relative path when computing container target path
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2024-09-20 17:00:23 +02:00
Guillaume Lours
9c60fe67df revert commits link to mount API over bind changes
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2024-09-20 10:37:57 +02:00
67 changed files with 1693 additions and 326 deletions

44
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,44 @@
# Security Policy
The maintainers of Docker Compose take security seriously. If you discover
a security issue, please bring it to their attention right away!
## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately
to [security@docker.com](mailto:security@docker.com).
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
received.
## Review Process
After receiving the report, an initial triage and technical analysis is
performed to confirm the report and determine its scope. We may request
additional information in this stage of the process.
Once a reviewer has confirmed the relevance of the report, a draft security
advisory will be created on GitHub. The draft advisory will be used to discuss
the issue with maintainers, the reporter(s), and where applicable, other
affected parties under embargo.
If the vulnerability is accepted, a timeline for developing a patch, public
disclosure, and patch release will be determined. If there is an embargo period
on public disclosure before the patch release, the reporter(s) are expected to
participate in the discussion of the timeline and abide by agreed upon dates
for public disclosure.
## Accreditation
Security reports are greatly appreciated and we will publicly thank you,
although we will keep your name confidential if you request it. We also like to
send gifts - if you're into swag, make sure to let us know. We do not currently
offer a paid security bounty program at this time.
## Supported Versions
This project docs not provide long-term supported versions, and only the current
release and `main` branch are actively maintained. Docker Compose v1, and the
corresponding [v1 branch](https://github.com/docker/compose/tree/v1) reached
EOL and are no longer supported.

View File

@@ -2,6 +2,15 @@
# to check if yaml reference docs used in this repo are valid
name: docs-upstream
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

View File

@@ -1,4 +1,14 @@
name: 'Close stale issues'
# Default to 'contents: read', which grants actions to read commits.
#
# If any permission is set, any permission not included in the list is
# implicitly set to "none".
#
# see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
permissions:
contents: read
on:
schedule:
- cron: '0 0 * * 0,3' # at midnight UTC every Sunday and Wednesday

View File

@@ -15,7 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
ARG GO_VERSION=1.22.7
ARG GO_VERSION=1.22.8
ARG XX_VERSION=1.2.1
ARG GOLANGCI_LINT_VERSION=v1.60.2
ARG ADDLICENSE_VERSION=v1.0.0

View File

@@ -23,6 +23,7 @@
people = [
"glours",
"jhrotko",
"milas",
"ndeloof",
"nicksieger",
@@ -72,6 +73,11 @@
Email = "guillaume.tardif@docker.com"
GitHub = "gtardif"
[people.jhrotko]
Name = "Joana Hrotko"
Email = "joana.hrotko@docker.com"
Github = "jhrotko"
[people.laurazard]
Name = "Laura Brehm"
Email = "laura.brehm@docker.com"

View File

@@ -33,6 +33,7 @@ func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
cmd.AddCommand(
vizCommand(p, dockerCli, backend),
publishCommand(p, dockerCli, backend),
generateCommand(p, backend),
)
return cmd
}

View File

@@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
@@ -37,6 +38,7 @@ import (
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/pkg/kvfile"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/internal/desktop"
"github.com/docker/compose/v2/internal/experimental"
@@ -70,6 +72,26 @@ const (
ComposeMenu = "COMPOSE_MENU"
)
// rawEnv load a dot env file using docker/cli key=value parser, without attempt to interpolate or evaluate values
func rawEnv(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error) {
lines, err := kvfile.ParseFromReader(r, lookup)
if err != nil {
return nil, fmt.Errorf("failed to parse env_file %s: %w", filename, err)
}
vars := types.Mapping{}
for _, line := range lines {
key, value, _ := strings.Cut(line, "=")
vars[key] = value
}
return vars, nil
}
func init() {
// compose evaluates env file values for interpolation
// `raw` format allows to load env_file with the same parser used by docker run --env-file
dotenv.RegisterFormat("raw", rawEnv)
}
type Backend interface {
api.Service
@@ -580,7 +602,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
}
c.AddCommand(
upCommand(&opts, dockerCli, backend, experiments),
upCommand(&opts, dockerCli, backend),
downCommand(&opts, dockerCli, backend),
startCommand(&opts, dockerCli, backend),
restartCommand(&opts, dockerCli, backend),
@@ -594,6 +616,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
removeCommand(&opts, dockerCli, backend),
execCommand(&opts, dockerCli, backend),
attachCommand(&opts, dockerCli, backend),
exportCommand(&opts, dockerCli, backend),
pauseCommand(&opts, dockerCli, backend),
unpauseCommand(&opts, dockerCli, backend),
topCommand(&opts, dockerCli, backend),

View File

@@ -66,9 +66,7 @@ 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.BoolVar(&opts.all, "all", false, "Include containers created by the run command")
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)")

74
cmd/compose/export.go Normal file
View File

@@ -0,0 +1,74 @@
/*
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 compose
import (
"context"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type exportOptions struct {
*ProjectOptions
service string
output string
index int
}
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
options := exportOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "export [OPTIONS] SERVICE",
Short: "Export a service container's filesystem as a tar archive",
Args: cobra.MinimumNArgs(1),
PreRunE: Adapt(func(ctx context.Context, args []string) error {
options.service = args[0]
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runExport(ctx, dockerCli, backend, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.IntVar(&options.index, "index", 0, "index of the container if service has multiple replicas.")
flags.StringVarP(&options.output, "output", "o", "", "Write to a file, instead of STDOUT")
return cmd
}
func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error {
projectName, err := options.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
exportOptions := api.ExportOptions{
Service: options.service,
Index: options.index,
Output: options.output,
}
return backend.Export(ctx, projectName, exportOptions)
}

82
cmd/compose/generate.go Normal file
View File

@@ -0,0 +1,82 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"os"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type generateOptions struct {
*ProjectOptions
Format string
}
func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
opts := generateOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "generate [OPTIONS] [CONTAINERS...]",
Short: "EXPERIMENTAL - Generate a Compose file from existing containers",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runGenerate(ctx, backend, opts, args)
}),
}
cmd.Flags().StringVar(&opts.ProjectName, "name", "", "Project name to set in the Compose file")
cmd.Flags().StringVar(&opts.ProjectDir, "project-dir", "", "Directory to use for the project")
cmd.Flags().StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
return cmd
}
func runGenerate(ctx context.Context, backend api.Service, opts generateOptions, containers []string) error {
_, _ = fmt.Fprintln(os.Stderr, "generate command is EXPERIMENTAL")
if len(containers) == 0 {
return fmt.Errorf("at least one container must be specified")
}
project, err := backend.Generate(ctx, api.GenerateOptions{
Containers: containers,
ProjectName: opts.ProjectName,
})
if err != nil {
return err
}
var content []byte
switch opts.Format {
case "json":
content, err = project.MarshalJSON()
case "yaml":
content, err = project.MarshalYAML()
default:
return fmt.Errorf("unsupported format %q", opts.Format)
}
if err != nil {
return err
}
fmt.Println(string(content))
return nil
}

View File

@@ -36,7 +36,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "publish [OPTIONS] [REPOSITORY]",
Use: "publish [OPTIONS] REPOSITORY[:TAG]",
Short: "Publish compose application",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPublish(ctx, dockerCli, backend, opts, args[0])

View File

@@ -27,7 +27,6 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/internal/experimental"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/spf13/cobra"
@@ -81,13 +80,19 @@ func (opts upOptions) apply(project *types.Project, services []string) (*types.P
return project, nil
}
func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli, experimentals *experimental.State) {
func (opts *upOptions) validateNavigationMenu(dockerCli command.Cli) {
if !dockerCli.Out().IsTerminal() {
opts.navigationMenu = false
return
}
// If --menu flag was not set
if !opts.navigationMenuChanged {
opts.navigationMenu = SetUnchangedOption(ComposeMenu, experimentals.NavBar())
if envVar, ok := os.LookupEnv(ComposeMenu); ok {
opts.navigationMenu = utils.StringToBool(envVar)
return
}
// ...and COMPOSE_MENU env var is not defined we want the default value to be true
opts.navigationMenu = true
}
}
@@ -102,7 +107,7 @@ func (opts upOptions) OnExit() api.Cascade {
}
}
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, experiments *experimental.State) *cobra.Command {
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
up := upOptions{}
create := createOptions{}
build := buildOptions{ProjectOptions: p}
@@ -127,7 +132,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service, ex
return errors.New("cannot combine --attach and --attach-dependencies")
}
up.validateNavigationMenu(dockerCli, experiments)
up.validateNavigationMenu(dockerCli)
if !p.All && len(project.Services) == 0 {
return fmt.Errorf("no service selected")

View File

@@ -21,7 +21,7 @@ import (
"strconv"
"sync"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/cli/cli/command"
)
var names = []string{
@@ -59,7 +59,7 @@ const (
)
// SetANSIMode configure formatter for colored output on ANSI-compliant console
func SetANSIMode(streams api.Streams, ansi string) {
func SetANSIMode(streams command.Streams, ansi string) {
if !useAnsi(streams, ansi) {
nextColor = func() colorFunc {
return monochrome
@@ -68,7 +68,7 @@ func SetANSIMode(streams api.Streams, ansi string) {
}
}
func useAnsi(streams api.Streams, ansi string) bool {
func useAnsi(streams command.Streams, ansi string) bool {
switch ansi {
case Always:
return true

View File

@@ -61,21 +61,32 @@ func (l *logConsumer) Register(name string) {
}
func (l *logConsumer) register(name string) *presenter {
cf := monochrome
if l.color {
if name == api.WatchLogger {
cf = makeColorFunc("92")
} else {
cf = nextColor()
var p *presenter
root, _, found := strings.Cut(name, " ")
if found {
parent := l.getPresenter(root)
p = &presenter{
colors: parent.colors,
name: name,
prefix: parent.prefix,
}
} else {
cf := monochrome
if l.color {
if name == api.WatchLogger {
cf = makeColorFunc("92")
} else {
cf = nextColor()
}
}
p = &presenter{
colors: cf,
name: name,
}
}
p := &presenter{
colors: cf,
name: name,
}
l.presenters.Store(name, p)
l.computeWidth()
if l.prefix {
l.computeWidth()
l.presenters.Range(func(key, value interface{}) bool {
p := value.(*presenter)
p.setPrefix(l.width)

View File

@@ -106,7 +106,6 @@ type LogKeyboard struct {
Watch KeyboardWatch
IsDockerDesktopActive bool
IsWatchConfigured bool
IsDDComposeUIActive bool
logLevel KEYBOARD_LOG_LEVEL
signalChannel chan<- os.Signal
}
@@ -114,7 +113,7 @@ type LogKeyboard struct {
var KeyboardManager *LogKeyboard
var eg multierror.Group
func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured, isDockerDesktopConfigActive bool,
func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured bool,
sc chan<- os.Signal,
watchFn func(ctx context.Context,
doneCh chan bool,
@@ -126,7 +125,6 @@ func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfi
km := LogKeyboard{}
km.IsDockerDesktopActive = isDockerDesktopActive
km.IsWatchConfigured = isWatchConfigured
km.IsDDComposeUIActive = isDockerDesktopConfigActive
km.logLevel = INFO
km.Watch.Watching = false
@@ -200,9 +198,10 @@ func (lk *LogKeyboard) navigationMenu() string {
if openDDInfo != "" {
openDDUI = navColor(" ")
}
if lk.IsDDComposeUIActive {
if lk.IsDockerDesktopActive {
openDDUI = openDDUI + shortcutKeyColor("o") + navColor(" View Config")
}
var watchInfo string
if openDDInfo != "" || openDDUI != "" {
watchInfo = navColor(" ")
@@ -246,7 +245,7 @@ func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Pro
}
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
if !lk.IsDDComposeUIActive {
if !lk.IsDockerDesktopActive {
return
}
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
@@ -288,19 +287,7 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
if !lk.IsWatchConfigured {
if lk.IsDDComposeUIActive {
// we try to open watch docs
lk.openDDWatchDocs(ctx, project)
}
// either way we mark menu/watch as an error
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := fmt.Errorf("Watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
lk.keyboardError("Watch", err)
return err
}))
return
}
lk.Watch.switchWatching()
if !lk.Watch.isWatching() {
@@ -330,6 +317,20 @@ func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Cont
case 'v':
lk.openDockerDesktop(ctx, project)
case 'w':
if !lk.IsWatchConfigured {
// we try to open watch docs if DD is installed
if lk.IsDockerDesktopActive {
lk.openDDWatchDocs(ctx, project)
}
// either way we mark menu/watch as an error
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
lk.keyboardError("Watch", err)
return err
}))
return
}
lk.StartWatch(ctx, doneCh, project, options)
case 'o':
lk.openDDComposeUI(ctx, project)

View File

@@ -19,6 +19,7 @@ Define and run multi-container applications with Docker
| [`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 |
| [`export`](compose_export.md) | Export a service container's filesystem as a tar archive |
| [`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 |

View File

@@ -0,0 +1,17 @@
# docker compose alpha generate
<!---MARKER_GEN_START-->
EXPERIMENTAL - Generate a Compose file from existing containers
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] |
| `--name` | `string` | | Project name to set in the Compose file |
| `--project-dir` | `string` | | Directory to use for the project |
<!---MARKER_GEN_END-->

View File

@@ -7,6 +7,7 @@ Copy files/folders between a service container and the local filesystem
| Name | Type | Default | Description |
|:----------------------|:-------|:--------|:--------------------------------------------------------|
| `--all` | `bool` | | Include containers created by the run command |
| `-a`, `--archive` | `bool` | | Archive mode (copy all uid/gid information) |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-L`, `--follow-link` | `bool` | | Always follow symbol link in SRC_PATH |

View File

@@ -0,0 +1,16 @@
# docker compose export
<!---MARKER_GEN_START-->
Export a service container's filesystem as a tar archive
### Options
| Name | Type | Default | Description |
|:-----------------|:---------|:--------|:---------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--index` | `int` | `0` | index of the container if service has multiple replicas. |
| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT |
<!---MARKER_GEN_END-->

View File

@@ -13,6 +13,7 @@ cname:
- docker compose down
- docker compose events
- docker compose exec
- docker compose export
- docker compose images
- docker compose kill
- docker compose logs
@@ -44,6 +45,7 @@ clink:
- docker_compose_down.yaml
- docker_compose_events.yaml
- docker_compose_exec.yaml
- docker_compose_export.yaml
- docker_compose_images.yaml
- docker_compose_kill.yaml
- docker_compose_logs.yaml

View File

@@ -4,9 +4,11 @@ long: Experimental commands
pname: docker compose
plink: docker_compose.yaml
cname:
- docker compose alpha generate
- docker compose alpha publish
- docker compose alpha viz
clink:
- docker_compose_alpha_generate.yaml
- docker_compose_alpha_publish.yaml
- docker_compose_alpha_viz.yaml
inherited_options:

View File

@@ -0,0 +1,53 @@
command: docker compose alpha generate
short: EXPERIMENTAL - Generate a Compose file from existing containers
long: EXPERIMENTAL - Generate a Compose file from existing containers
usage: docker compose alpha generate [OPTIONS] [CONTAINERS...]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:
- option: format
value_type: string
default_value: yaml
description: 'Format the output. Values: [yaml | json]'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: name
value_type: string
description: Project name to set in the Compose file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: project-dir
value_type: string
description: Directory to use for the project
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: true
kubernetes: false
swarm: false

View File

@@ -1,7 +1,7 @@
command: docker compose alpha publish
short: Publish compose application
long: Publish compose application
usage: docker compose alpha publish [OPTIONS] [REPOSITORY]
usage: docker compose alpha publish [OPTIONS] REPOSITORY[:TAG]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:

View File

@@ -10,9 +10,9 @@ options:
- option: all
value_type: bool
default_value: "false"
description: Copy to all the containers of the service
deprecated: true
hidden: true
description: Include containers created by the run command
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -0,0 +1,45 @@
command: docker compose export
short: Export a service container's filesystem as a tar archive
long: Export a service container's filesystem as a tar archive
usage: docker compose export [OPTIONS] SERVICE
pname: docker compose
plink: docker_compose.yaml
options:
- option: index
value_type: int
default_value: "0"
description: index of the container if service has multiple replicas.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: output
shorthand: o
value_type: string
description: Write to a file, instead of STDOUT
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

28
go.mod
View File

@@ -1,21 +1,21 @@
module github.com/docker/compose/v2
go 1.21.0
go 1.22.0
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Microsoft/go-winio v0.6.2
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/buger/goterm v1.0.4
github.com/compose-spec/compose-go/v2 v2.2.0
github.com/containerd/containerd v1.7.22
github.com/compose-spec/compose-go/v2 v2.4.4
github.com/containerd/containerd v1.7.23
github.com/containerd/platforms v0.2.1
github.com/davecgh/go-spew v1.1.1
github.com/distribution/reference v0.6.0
github.com/docker/buildx v0.17.1
github.com/docker/cli v27.3.0-rc.2+incompatible
github.com/docker/cli v27.3.2-0.20241008150905-cb3048fbebb1+incompatible
github.com/docker/cli-docs-tool v0.8.0
github.com/docker/docker v27.3.0-rc.2+incompatible
github.com/docker/docker v27.3.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
@@ -50,13 +50,14 @@ require (
go.opentelemetry.io/otel/sdk v1.21.0
go.opentelemetry.io/otel/trace v1.21.0
go.uber.org/goleak v1.3.0
go.uber.org/mock v0.4.0
go.uber.org/mock v0.5.0
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
golang.org/x/sync v0.8.0
golang.org/x/sys v0.25.0
google.golang.org/grpc v1.62.0
golang.org/x/sys v0.26.0
google.golang.org/grpc v1.67.1
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
tags.cncf.io/container-device-interface v0.8.0
)
require (
@@ -79,11 +80,11 @@ require (
github.com/aws/smithy-go v1.19.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/containerd/api v1.7.19 // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/containerd/errdefs v0.1.0 // indirect
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/ttrpc v1.2.5 // indirect
github.com/containerd/typeurl/v2 v2.2.0 // indirect
@@ -175,9 +176,9 @@ require (
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -190,5 +191,4 @@ require (
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
tags.cncf.io/container-device-interface v0.8.0 // indirect
)

64
go.sum
View File

@@ -1,7 +1,7 @@
cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
@@ -77,28 +77,28 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ=
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM=
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg=
github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
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.2.0 h1:VsQosGhuO+H9wh5laiIiAe4TVd73kQ5NWwmNrdm0HRA=
github.com/compose-spec/compose-go/v2 v2.2.0/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
github.com/compose-spec/compose-go/v2 v2.4.4 h1:cvHBl5Jf1iNBmRrZCICmHvaoskYc1etTPEMLKVwokAY=
github.com/compose-spec/compose-go/v2 v2.4.4/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
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.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd v1.7.22 h1:nZuNnNRA6T6jB975rx2RRNqqH2k6ELYKDZfqTHqwyy0=
github.com/containerd/containerd v1.7.22/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g=
github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ=
github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA=
github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig=
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
@@ -128,15 +128,15 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/buildx v0.17.1 h1:9ob2jGp4+W9PxWw68GsoNFp+eYFc7eUoRL9VljLCSM4=
github.com/docker/buildx v0.17.1/go.mod h1:kJOhOhS47LRvrLFRulFiO5SE6VJf54yYMn7DzjgO5W0=
github.com/docker/cli v27.3.0-rc.2+incompatible h1:z/lQXreGSzWl0RySxyFOm6WygLQtre2EkB4SCwsEut0=
github.com/docker/cli v27.3.0-rc.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.3.2-0.20241008150905-cb3048fbebb1+incompatible h1:fJ3SzYiebfWoas3qOJpmRsNrDL2w1XIpPFywSLFhhfk=
github.com/docker/cli v27.3.2-0.20241008150905-cb3048fbebb1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.8.0 h1:YcDWl7rQJC3lJ7WVZRwSs3bc9nka97QLWfyJQli8yJU=
github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsBIrW21a5pUbdk=
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 v27.3.0-rc.2+incompatible h1:+WSuhLOwbip2IuVKZ4ecSQfG/grbPw96d3b7jBC8MSQ=
github.com/docker/docker v27.3.0-rc.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@@ -158,8 +158,8 @@ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJ
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
@@ -199,8 +199,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -520,8 +520,8 @@ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lI
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -582,8 +582,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.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=
@@ -610,15 +610,15 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=

View File

@@ -27,11 +27,15 @@ import (
"net/http"
"strings"
"github.com/docker/compose/v2/internal"
"github.com/docker/compose/v2/internal/memnet"
"github.com/r3labs/sse"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
// identify this client in the logs
var userAgent = "compose/" + internal.Version
// Client for integration with Docker Desktop features.
type Client struct {
apiEndpoint string
@@ -76,6 +80,7 @@ func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
resp, err := c.client.Do(req)
if err != nil {
return nil, err
@@ -105,6 +110,7 @@ func (c *Client) FeatureFlags(ctx context.Context) (FeatureFlagResponse, error)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
resp, err := c.client.Do(req)
if err != nil {
return nil, err
@@ -135,6 +141,7 @@ func (c *Client) GetFileSharesConfig(ctx context.Context) (*GetFileSharesConfigR
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
resp, err := c.client.Do(req)
if err != nil {
return nil, err
@@ -170,6 +177,7 @@ func (c *Client) CreateFileShare(ctx context.Context, r CreateFileShareRequest)
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent)
resp, err := c.client.Do(req)
if err != nil {
return nil, err
@@ -212,6 +220,7 @@ func (c *Client) ListFileShares(ctx context.Context) ([]FileShareSession, error)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
resp, err := c.client.Do(req)
if err != nil {
return nil, err
@@ -236,6 +245,7 @@ func (c *Client) DeleteFileShare(ctx context.Context, id string) error {
if err != nil {
return err
}
req.Header.Set("User-Agent", userAgent)
resp, err := c.client.Do(req)
if err != nil {
return err
@@ -268,6 +278,7 @@ func (c *Client) StreamFileShares(ctx context.Context) (<-chan EventMessage[[]Fi
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
resp, err := c.client.Do(req)
if err != nil {
return nil, err

View File

@@ -66,20 +66,3 @@ func (s *State) Load(ctx context.Context, client *desktop.Client) error {
s.desktopValues = desktopValues
return nil
}
func (s *State) NavBar() bool {
return s.determineFeatureState("ComposeNav")
}
func (s *State) ComposeUI() bool {
return s.determineFeatureState("ComposeUIView")
}
func (s *State) determineFeatureState(name string) bool {
if s == nil || !s.active || s.desktopValues == nil {
return false
}
// TODO(milas): we should add individual environment variable overrides
// per-experiment in a generic way here
return s.desktopValues[name].Enabled
}

View File

@@ -179,7 +179,7 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pusha
config = v1.DescriptorEmptyJSON
artifactType = ComposeProjectArtifactType
// N.B. the descriptor has the data embedded in it
toPush = append(toPush, Pushable{Descriptor: config, Data: nil})
toPush = append(toPush, Pushable{Descriptor: config, Data: make([]byte, len(config.Data))})
default:
return nil, fmt.Errorf("unsupported OCI version: %s", ociCompat)
}

View File

@@ -17,10 +17,8 @@
package tracing
import (
"context"
"fmt"
"os"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
@@ -63,11 +61,7 @@ func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptr
}
}
dialCtx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
conn, err := grpc.DialContext(
dialCtx,
cfg.Endpoint,
conn, err := grpc.NewClient(cfg.Endpoint,
grpc.WithContextDialer(memnet.DialEndpoint),
// this dial is restricted to using a local Unix socket / named pipe,
// so there is no need for TLS

View File

@@ -22,19 +22,16 @@ import (
"go.opentelemetry.io/otel/attribute"
)
func KeyboardMetrics(ctx context.Context, enabled, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI bool) {
func KeyboardMetrics(ctx context.Context, enabled, isDockerDesktopActive, isWatchConfigured bool) {
commandAvailable := []string{}
if isDockerDesktopActive {
commandAvailable = append(commandAvailable, "gui")
commandAvailable = append(commandAvailable, "gui/composeview")
}
if isWatchConfigured {
commandAvailable = append(commandAvailable, "watch")
}
if isDockerDesktopComposeUI {
commandAvailable = append(commandAvailable, "gui/composeview")
}
AddAttributeToSpan(ctx,
attribute.Bool("navmenu.enabled", enabled),
attribute.StringSlice("navmenu.command_available", commandAvailable))

View File

@@ -90,6 +90,10 @@ type Service interface {
Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error)
// Scale manages numbers of container instances running per service
Scale(ctx context.Context, project *types.Project, options ScaleOptions) error
// Export a service container's filesystem as a tar archive
Export(ctx context.Context, projectName string, options ExportOptions) error
// Generate generates a Compose Project from existing containers
Generate(ctx context.Context, options GenerateOptions) (*types.Project, error)
}
type ScaleOptions struct {
@@ -289,6 +293,7 @@ type ConfigOptions struct {
type PushOptions struct {
Quiet bool
IgnoreFailures bool
ImageMandatory bool
}
// PullOptions group options of the Pull API
@@ -553,6 +558,20 @@ type PauseOptions struct {
Project *types.Project
}
// ExportOptions group options of the Export API
type ExportOptions struct {
Service string
Index int
Output string
}
type GenerateOptions struct {
// ProjectName to set in the Compose file
ProjectName string
// Containers passed in the command line to be used as reference for service definition
Containers []string
}
const (
// STARTING indicates that stack is being deployed
STARTING string = "Starting"
@@ -628,6 +647,8 @@ const (
ContainerEventExit
// UserCancel user cancelled compose up, we are stopping containers
UserCancel
// HookEventLog is a ContainerEvent of type log on stdout by service hook
HookEventLog
)
// Separator is used for naming components

View File

@@ -176,7 +176,10 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
}
set := types.Services{}
for _, c := range containers {
serviceLabel := c.Labels[api.ServiceLabel]
serviceLabel, ok := c.Labels[api.ServiceLabel]
if !ok {
serviceLabel = getCanonicalContainerName(c)
}
service, ok := set[serviceLabel]
if !ok {
service = types.ServiceConfig{
@@ -321,30 +324,6 @@ func (s *composeService) RuntimeVersion(ctx context.Context) (string, error) {
}
var windowsContainer = struct {
once sync.Once
val bool
err error
}{}
func (s *composeService) isWindowsContainer(ctx context.Context) (bool, error) {
windowsContainer.once.Do(func() {
info, err := s.apiClient().Info(ctx)
if err != nil {
windowsContainer.err = err
}
windowsContainer.val = info.OSType == "windows"
})
return windowsContainer.val, windowsContainer.err
}
func (s *composeService) isDesktopIntegrationActive() bool {
return s.desktopCli != nil
}
func (s *composeService) isDesktopUIEnabled() bool {
if !s.isDesktopIntegrationActive() {
return false
}
return s.experiments.ComposeUI()
}

View File

@@ -149,7 +149,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
container := container
traceOpts := append(tracing.ServiceOptions(service), tracing.ContainerOptions(container)...)
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "service/scale/down", traceOpts, func(ctx context.Context) error {
return c.service.stopAndRemoveContainer(ctx, container, timeout, false)
return c.service.stopAndRemoveContainer(ctx, container, &service, timeout, false)
}))
continue
}
@@ -223,8 +223,8 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
// Stop dependent containers, so they will be restarted after service is re-created
dependents := project.GetDependentsForService(service)
for _, name := range dependents {
dependents := c.observedState[name]
err := c.service.stopContainers(ctx, w, dependents, nil)
dependents := c.getObservedState(name)
err := c.service.stopContainers(ctx, w, &service, dependents, nil)
if err != nil {
return err
}
@@ -232,7 +232,7 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
dependent.State = ContainerExited
dependents[i] = dependent
}
c.observedState[name] = dependents
c.setObservedState(name, dependents)
}
return nil
}
@@ -364,7 +364,12 @@ func containerReasonEvents(containers Containers, eventFunc func(string, string)
const ServiceConditionRunningOrHealthy = "running_or_healthy"
//nolint:gocyclo
func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, dependant string, dependencies types.DependsOnConfig, containers Containers) error {
func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, dependant string, dependencies types.DependsOnConfig, containers Containers, timeout time.Duration) error {
if timeout > 0 {
withTimeout, cancelFunc := context.WithTimeout(ctx, timeout)
defer cancelFunc()
ctx = withTimeout
}
eg, _ := errgroup.WithContext(ctx)
w := progress.ContextWriter(ctx)
for dep, config := range dependencies {
@@ -454,7 +459,11 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
}
})
}
return eg.Wait()
err := eg.Wait()
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("timeout waiting for dependencies")
}
return err
}
func shouldWaitForDependency(serviceName string, dependencyConfig types.ServiceDependency, project *types.Project) (bool, error) {
@@ -760,12 +769,15 @@ func (s *composeService) isServiceCompleted(ctx context.Context, containers Cont
return false, 0, nil
}
func (s *composeService) startService(ctx context.Context, project *types.Project, service types.ServiceConfig, containers Containers) error {
func (s *composeService) startService(ctx context.Context,
project *types.Project, service types.ServiceConfig,
containers Containers, listener api.ContainerEventListener,
timeout time.Duration) error {
if service.Deploy != nil && service.Deploy.Replicas != nil && *service.Deploy.Replicas == 0 {
return nil
}
err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, containers)
err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, containers, timeout)
if err != nil {
return err
}
@@ -784,10 +796,18 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
}
eventName := getContainerProgressName(container)
w.Event(progress.StartingEvent(eventName))
err := s.apiClient().ContainerStart(ctx, container.ID, containerType.StartOptions{})
err = s.apiClient().ContainerStart(ctx, container.ID, containerType.StartOptions{})
if err != nil {
return err
}
for _, hook := range service.PostStart {
err = s.runHook(ctx, container, service, hook, listener)
if err != nil {
return err
}
}
w.Event(progress.StartedEvent(eventName))
}
return nil

View File

@@ -28,6 +28,7 @@ import (
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"
@@ -238,7 +239,7 @@ func TestWaitDependencies(t *testing.T) {
"db": {Condition: ServiceConditionRunningOrHealthy},
"redis": {Condition: ServiceConditionRunningOrHealthy},
}
assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil))
assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil, 0))
})
t.Run("should skip dependencies with condition service_started", func(t *testing.T) {
dbService := types.ServiceConfig{Name: "db", Scale: intPtr(1)}
@@ -251,7 +252,7 @@ func TestWaitDependencies(t *testing.T) {
"db": {Condition: types.ServiceConditionStarted, Required: true},
"redis": {Condition: types.ServiceConditionStarted, Required: true},
}
assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil))
assert.NilError(t, tested.waitDependencies(context.Background(), &project, "", dependencies, nil, 0))
})
}
@@ -300,10 +301,17 @@ func TestCreateMobyContainer(t *testing.T) {
},
}
apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Cond(func(x any) bool {
v := x.(*containerType.HostConfig)
return v.NetworkMode == "b-moby-name"
}), gomock.Eq(
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": {
@@ -382,10 +390,17 @@ func TestCreateMobyContainer(t *testing.T) {
},
}
apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any(), gomock.Cond(func(x any) bool {
v := x.(*containerType.HostConfig)
return v.NetworkMode == "b-moby-name"
}), gomock.Eq(
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": {

View File

@@ -61,11 +61,6 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
direction |= fromService
serviceName = srcService
copyFunc = s.copyFromContainer
// copying from multiple containers of a services doesn't make sense.
if options.All {
return errors.New("cannot use the --all flag when copying from a service")
}
}
if destService != "" {
direction |= toService
@@ -80,7 +75,7 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
return errors.New("unknown copy direction")
}
containers, err := s.listContainersTargetedForCopy(ctx, projectName, options.Index, direction, serviceName)
containers, err := s.listContainersTargetedForCopy(ctx, projectName, options, direction, serviceName)
if err != nil {
return err
}
@@ -119,18 +114,22 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
return g.Wait()
}
func (s *composeService) listContainersTargetedForCopy(ctx context.Context, projectName string, index int, direction copyDirection, serviceName string) (Containers, error) {
func (s *composeService) listContainersTargetedForCopy(ctx context.Context, projectName string, options api.CopyOptions, direction copyDirection, serviceName string) (Containers, error) {
var containers Containers
var err error
switch {
case index > 0:
ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, true, serviceName, index)
case options.Index > 0:
ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, true, serviceName, options.Index)
if err != nil {
return nil, err
}
return append(containers, ctr), nil
default:
containers, err = s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
withOneOff := oneOffExclude
if options.All {
withOneOff = oneOffInclude
}
containers, err = s.getContainers(ctx, projectName, withOneOff, true, serviceName)
if err != nil {
return nil, err
}

View File

@@ -24,7 +24,6 @@ import (
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strconv"
@@ -48,6 +47,7 @@ import (
"github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat"
"github.com/sirupsen/logrus"
cdi "tags.cncf.io/container-device-interface/pkg/parser"
)
type createOptions struct {
@@ -104,7 +104,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
orphans := observedState.filter(isOrphaned(project))
if len(orphans) > 0 && !options.IgnoreOrphans {
if options.RemoveOrphans {
err := s.removeContainers(ctx, orphans, nil, false)
err := s.removeContainers(ctx, orphans, nil, nil, false)
if err != nil {
return err
}
@@ -505,7 +505,10 @@ func (s *composeService) prepareLabels(labels types.Labels, service types.Servic
}
labels[api.ConfigHashLabel] = hash
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
if number > 0 {
// One-off containers are not indexed
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
}
var dependencies []string
for s, d := range service.DependsOn {
@@ -645,7 +648,14 @@ func getDeployResources(s types.ServiceConfig) container.Resources {
setReservations(s.Deploy.Resources.Reservations, &resources)
}
var cdiDeviceNames []string
for _, device := range s.Devices {
if device.Source == device.Target && cdi.IsQualifiedName(device.Source) {
cdiDeviceNames = append(cdiDeviceNames, device.Source)
continue
}
resources.Devices = append(resources.Devices, container.DeviceMapping{
PathOnHost: device.Source,
PathInContainer: device.Target,
@@ -653,6 +663,23 @@ func getDeployResources(s types.ServiceConfig) container.Resources {
})
}
if len(cdiDeviceNames) > 0 {
resources.DeviceRequests = append(resources.DeviceRequests, container.DeviceRequest{
Driver: "cdi",
DeviceIDs: cdiDeviceNames,
})
}
for _, gpus := range s.Gpus {
resources.DeviceRequests = append(resources.DeviceRequests, container.DeviceRequest{
Driver: gpus.Driver,
Count: int(gpus.Count),
DeviceIDs: gpus.IDs,
Capabilities: [][]string{append(gpus.Capabilities, "gpu")},
Options: gpus.Options,
})
}
ulimits := toUlimits(s.Ulimits)
resources.Ulimits = ulimits
return resources
@@ -694,6 +721,7 @@ func setReservations(reservations *types.Resource, resources *container.Resource
Count: int(device.Count),
DeviceIDs: device.IDs,
Driver: device.Driver,
Options: device.Options,
})
}
}
@@ -801,28 +829,28 @@ func (s *composeService) buildContainerVolumes(
return nil, nil, err
}
mountOptions, err := s.buildContainerMountOptions(ctx, p, service, imgInspect, inherit)
mountOptions, err := buildContainerMountOptions(p, service, imgInspect, inherit)
if err != nil {
return nil, nil, err
}
version, err := s.RuntimeVersion(ctx)
if err != nil {
return nil, nil, err
}
if versions.GreaterThan(version, "1.42") {
// We can fully leverage `Mount` API as a replacement for legacy `Bind`
return nil, mountOptions, nil
}
MOUNTS:
for _, m := range mountOptions {
if m.Type == mount.TypeNamedPipe {
mounts = append(mounts, m)
continue
}
if m.Type == mount.TypeBind {
// `Mount` does not offer option to created host path if missing
// `Mount` is preferred but does not offer option to created host path if missing
// so `Bind` API is used here with raw volume string
// see https://github.com/moby/moby/issues/43483
for _, v := range service.Volumes {
if v.Target == m.Target {
if v.Bind != nil && v.Bind.CreateHostPath {
switch {
case string(m.Type) != v.Type:
v.Source = m.Source
fallthrough
case !requireMountAPI(v.Bind):
binds = append(binds, v.String())
continue MOUNTS
}
@@ -834,7 +862,24 @@ MOUNTS:
return binds, mounts, nil
}
func (s *composeService) buildContainerMountOptions(ctx context.Context, p types.Project, service types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
// requireMountAPI check if Bind declaration can be implemented by the plain old Bind API or uses any of the advanced
// options which require use of Mount API
func requireMountAPI(bind *types.ServiceVolumeBind) bool {
switch {
case bind == nil:
return false
case !bind.CreateHostPath:
return true
case bind.Propagation != "":
return true
case bind.Recursive != "":
return true
default:
return false
}
}
func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
var mounts = map[string]mount.Mount{}
if inherit != nil {
for _, m := range inherit.Mounts {
@@ -845,7 +890,6 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
if m.Type == "volume" {
src = m.Name
}
m.Destination = path.Clean(m.Destination)
if img.Config != nil {
if _, ok := img.Config.Volumes[m.Destination]; ok {
@@ -859,7 +903,7 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
}
}
volumes := []types.ServiceVolumeConfig{}
for _, v := range service.Volumes {
for _, v := range s.Volumes {
if v.Target != m.Destination || v.Source != "" {
volumes = append(volumes, v)
continue
@@ -872,11 +916,11 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
ReadOnly: !m.RW,
}
}
service.Volumes = volumes
s.Volumes = volumes
}
}
mounts, err := s.fillBindMounts(ctx, p, service, mounts)
mounts, err := fillBindMounts(p, s, mounts)
if err != nil {
return nil, err
}
@@ -888,27 +932,27 @@ func (s *composeService) buildContainerMountOptions(ctx context.Context, p types
return values, nil
}
func (s *composeService) fillBindMounts(ctx context.Context, p types.Project, service types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) {
for _, v := range service.Volumes {
bindMount, err := s.buildMount(ctx, p, v)
func fillBindMounts(p types.Project, s types.ServiceConfig, m map[string]mount.Mount) (map[string]mount.Mount, error) {
for _, v := range s.Volumes {
bindMount, err := buildMount(p, v)
if err != nil {
return nil, err
}
m[bindMount.Target] = bindMount
}
secrets, err := s.buildContainerSecretMounts(ctx, p, service)
secrets, err := buildContainerSecretMounts(p, s)
if err != nil {
return nil, err
}
for _, secret := range secrets {
if _, found := m[secret.Target]; found {
for _, s := range secrets {
if _, found := m[s.Target]; found {
continue
}
m[secret.Target] = secret
m[s.Target] = s
}
configs, err := s.buildContainerConfigMounts(ctx, p, service)
configs, err := buildContainerConfigMounts(p, s)
if err != nil {
return nil, err
}
@@ -921,11 +965,11 @@ func (s *composeService) fillBindMounts(ctx context.Context, p types.Project, se
return m, nil
}
func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types.Project, service types.ServiceConfig) ([]mount.Mount, error) {
func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
var mounts = map[string]mount.Mount{}
configsBaseDir := "/"
for _, config := range service.Configs {
for _, config := range s.Configs {
target := config.Target
if config.Target == "" {
target = configsBaseDir + config.Source
@@ -933,10 +977,6 @@ func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types
target = configsBaseDir + config.Target
}
if config.UID != "" || config.GID != "" || config.Mode != nil {
logrus.Warn("config `uid`, `gid` and `mode` are not supported, they will be ignored")
}
definedConfig := p.Configs[config.Source]
if definedConfig.External {
return nil, fmt.Errorf("unsupported external config %s", definedConfig.Name)
@@ -953,7 +993,11 @@ func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types
continue
}
bindMount, err := s.buildMount(ctx, p, types.ServiceVolumeConfig{
if config.UID != "" || config.GID != "" || config.Mode != nil {
logrus.Warn("config `uid`, `gid` and `mode` are not supported, they will be ignored")
}
bindMount, err := buildMount(p, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind,
Source: definedConfig.File,
Target: target,
@@ -971,11 +1015,11 @@ func (s *composeService) buildContainerConfigMounts(ctx context.Context, p types
return values, nil
}
func (s *composeService) buildContainerSecretMounts(ctx context.Context, p types.Project, service types.ServiceConfig) ([]mount.Mount, error) {
func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
var mounts = map[string]mount.Mount{}
secretsDir := "/run/secrets/"
for _, secret := range service.Secrets {
for _, secret := range s.Secrets {
target := secret.Target
if secret.Target == "" {
target = secretsDir + secret.Source
@@ -983,10 +1027,6 @@ func (s *composeService) buildContainerSecretMounts(ctx context.Context, p types
target = secretsDir + secret.Target
}
if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
logrus.Warn("secrets `uid`, `gid` and `mode` are not supported, they will be ignored")
}
definedSecret := p.Secrets[secret.Source]
if definedSecret.External {
return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name)
@@ -1003,11 +1043,22 @@ func (s *composeService) buildContainerSecretMounts(ctx context.Context, p types
continue
}
mnt, err := s.buildMount(ctx, p, types.ServiceVolumeConfig{
if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
logrus.Warn("secrets `uid`, `gid` and `mode` are not supported, they will be ignored")
}
if _, err := os.Stat(definedSecret.File); os.IsNotExist(err) {
logrus.Warnf("secret file %s does not exist", definedSecret.Name)
}
mnt, err := buildMount(p, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind,
Source: definedSecret.File,
Target: target,
ReadOnly: true,
Bind: &types.ServiceVolumeBind{
CreateHostPath: false,
},
})
if err != nil {
return nil, err
@@ -1039,7 +1090,7 @@ func isWindowsAbs(p string) bool {
return false
}
func (s *composeService) buildMount(ctx context.Context, project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
source := volume.Source
// on windows, filepath.IsAbs(source) is false for unix style abs path like /var/run/docker.sock.
// do not replace these with filepath.Abs(source) that will include a default drive.
@@ -1060,12 +1111,7 @@ func (s *composeService) buildMount(ctx context.Context, project types.Project,
}
}
bind, vol, tmpfs, err := s.buildMountOptions(ctx, volume)
if err != nil {
return mount.Mount{}, err
}
volume.Target = path.Clean(volume.Target)
bind, vol, tmpfs := buildMountOptions(volume)
if bind != nil {
volume.Type = types.VolumeTypeBind
@@ -1083,7 +1129,7 @@ func (s *composeService) buildMount(ctx context.Context, project types.Project,
}, nil
}
func (s *composeService) buildMountOptions(ctx context.Context, volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions, error) {
func buildMountOptions(volume types.ServiceVolumeConfig) (*mount.BindOptions, *mount.VolumeOptions, *mount.TmpfsOptions) {
switch volume.Type {
case "bind":
if volume.Volume != nil {
@@ -1092,8 +1138,7 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
if volume.Tmpfs != nil {
logrus.Warnf("mount of type `bind` should not define `tmpfs` option")
}
option, err := s.buildBindOption(ctx, volume.Bind)
return option, nil, nil, err
return buildBindOption(volume.Bind), nil, nil
case "volume":
if volume.Bind != nil {
logrus.Warnf("mount of type `volume` should not define `bind` option")
@@ -1101,7 +1146,7 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
if volume.Tmpfs != nil {
logrus.Warnf("mount of type `volume` should not define `tmpfs` option")
}
return nil, buildVolumeOptions(volume.Volume), nil, nil
return nil, buildVolumeOptions(volume.Volume), nil
case "tmpfs":
if volume.Bind != nil {
logrus.Warnf("mount of type `tmpfs` should not define `bind` option")
@@ -1109,30 +1154,28 @@ func (s *composeService) buildMountOptions(ctx context.Context, volume types.Ser
if volume.Volume != nil {
logrus.Warnf("mount of type `tmpfs` should not define `volume` option")
}
return nil, nil, buildTmpfsOptions(volume.Tmpfs), nil
return nil, nil, buildTmpfsOptions(volume.Tmpfs)
}
return nil, nil, nil, nil
return nil, nil, nil
}
func (s *composeService) buildBindOption(ctx context.Context, bind *types.ServiceVolumeBind) (*mount.BindOptions, error) {
func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
if bind == nil {
return nil, nil
return nil
}
propagation := bind.Propagation
isWindowsContainer, err := s.isWindowsContainer(ctx)
if err != nil {
return nil, err
}
if propagation == "" && !isWindowsContainer {
propagation = types.PropagationRPrivate
}
return &mount.BindOptions{
Propagation: mount.Propagation(propagation),
opts := &mount.BindOptions{
Propagation: mount.Propagation(bind.Propagation),
CreateMountpoint: bind.CreateHostPath,
// NonRecursive: false, FIXME missing from model ?
}, nil
}
switch bind.Recursive {
case "disabled":
opts.NonRecursive = true
case "writable":
opts.ReadOnlyNonRecursive = true
case "readonly":
opts.ReadOnlyForceRecursive = true
}
return opts
}
func buildVolumeOptions(vol *types.ServiceVolumeVolume) *mount.VolumeOptions {
@@ -1190,7 +1233,13 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, n *types.Ne
"Set `external: true` to use an existing network", n.Name, expectedProjectLabel)
}
if inspect.Labels[api.NetworkLabel] != expectedNetworkLabel {
return fmt.Errorf("network %s was found but has incorrect label %s set to %q", n.Name, api.NetworkLabel, inspect.Labels[api.NetworkLabel])
return fmt.Errorf(
"network %s was found but has incorrect label %s set to %q (expected: %q)",
n.Name,
api.NetworkLabel,
inspect.Labels[api.NetworkLabel],
expectedNetworkLabel,
)
}
return nil
}
@@ -1314,7 +1363,6 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
switch len(networks) {
case 1:
n.Name = networks[0].ID
return nil
case 0:
enabled, err := s.isSWarmEnabled(ctx)

View File

@@ -17,7 +17,6 @@
package compose
import (
"context"
"os"
"path/filepath"
"sort"
@@ -42,8 +41,7 @@ func TestBuildBindMount(t *testing.T) {
Source: "",
Target: "/data",
}
s := composeService{}
mount, err := s.buildMount(context.TODO(), project, volume)
mount, err := buildMount(project, volume)
assert.NilError(t, err)
assert.Assert(t, filepath.IsAbs(mount.Source))
_, err = os.Stat(mount.Source)
@@ -58,8 +56,7 @@ func TestBuildNamedPipeMount(t *testing.T) {
Source: "\\\\.\\pipe\\docker_engine_windows",
Target: "\\\\.\\pipe\\docker_engine",
}
s := composeService{}
mount, err := s.buildMount(context.TODO(), project, volume)
mount, err := buildMount(project, volume)
assert.NilError(t, err)
assert.Equal(t, mount.Type, mountTypes.TypeNamedPipe)
}
@@ -78,8 +75,7 @@ func TestBuildVolumeMount(t *testing.T) {
Source: "myVolume",
Target: "/data",
}
s := composeService{}
mount, err := s.buildMount(context.TODO(), project, volume)
mount, err := buildMount(project, volume)
assert.NilError(t, err)
assert.Equal(t, mount.Source, "myProject_myVolume")
assert.Equal(t, mount.Type, mountTypes.TypeVolume)
@@ -156,8 +152,8 @@ func TestBuildContainerMountOptions(t *testing.T) {
},
},
}
s := composeService{}
mounts, err := s.buildContainerMountOptions(context.TODO(), project, project.Services["myService"], moby.ImageInspect{}, inherit)
mounts, err := buildContainerMountOptions(project, project.Services["myService"], moby.ImageInspect{}, inherit)
sort.Slice(mounts, func(i, j int) bool {
return mounts[i].Target < mounts[j].Target
})
@@ -169,7 +165,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
assert.Equal(t, mounts[2].VolumeOptions.Subpath, "etc")
assert.Equal(t, mounts[3].Target, "\\\\.\\pipe\\docker_engine")
mounts, err = s.buildContainerMountOptions(context.TODO(), project, project.Services["myService"], moby.ImageInspect{}, inherit)
mounts, err = buildContainerMountOptions(project, project.Services["myService"], moby.ImageInspect{}, inherit)
sort.Slice(mounts, func(i, j int) bool {
return mounts[i].Target < mounts[j].Target
})

View File

@@ -67,18 +67,26 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
}
// Check requested services exists in model
options.Services, err = checkSelectedServices(options, project)
services, err := checkSelectedServices(options, project)
if err != nil {
return err
}
if len(options.Services) > 0 && len(services) == 0 {
logrus.Infof("Any of the services %v not running in project %q", options.Services, projectName)
return nil
}
options.Services = services
if len(containers) > 0 {
resourceToRemove = true
}
err = InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
serviceContainers := containers.filter(isService(service))
err := s.removeContainers(ctx, serviceContainers, options.Timeout, options.Volumes)
serv := project.Services[service]
err := s.removeContainers(ctx, serviceContainers, &serv, options.Timeout, options.Volumes)
return err
}, WithRootNodesAndDown(options.Services))
if err != nil {
@@ -87,7 +95,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
orphans := containers.filter(isOrphaned(project))
if options.RemoveOrphans && len(orphans) > 0 {
err := s.removeContainers(ctx, orphans, options.Timeout, false)
err := s.removeContainers(ctx, orphans, nil, options.Timeout, false)
if err != nil {
return err
}
@@ -289,9 +297,19 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
return err
}
func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, container moby.Container, timeout *time.Duration) error {
func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, service *types.ServiceConfig, container moby.Container, timeout *time.Duration) error {
eventName := getContainerProgressName(container)
w.Event(progress.StoppingEvent(eventName))
if service != nil {
for _, hook := range service.PreStop {
err := s.runHook(ctx, container, *service, hook, nil)
if err != nil {
return err
}
}
}
timeoutInSecond := utils.DurationSecondToInt(timeout)
err := s.apiClient().ContainerStop(ctx, container.ID, containerType.StopOptions{Timeout: timeoutInSecond})
if err != nil {
@@ -302,32 +320,32 @@ func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, c
return nil
}
func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, serv *types.ServiceConfig, containers []moby.Container, timeout *time.Duration) error {
eg, ctx := errgroup.WithContext(ctx)
for _, container := range containers {
container := container
eg.Go(func() error {
return s.stopContainer(ctx, w, container, timeout)
return s.stopContainer(ctx, w, serv, container, timeout)
})
}
return eg.Wait()
}
func (s *composeService) removeContainers(ctx context.Context, containers []moby.Container, timeout *time.Duration, volumes bool) error {
func (s *composeService) removeContainers(ctx context.Context, containers []moby.Container, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
eg, _ := errgroup.WithContext(ctx)
for _, container := range containers {
container := container
eg.Go(func() error {
return s.stopAndRemoveContainer(ctx, container, timeout, volumes)
return s.stopAndRemoveContainer(ctx, container, service, timeout, volumes)
})
}
return eg.Wait()
}
func (s *composeService) stopAndRemoveContainer(ctx context.Context, container moby.Container, timeout *time.Duration, volumes bool) error {
func (s *composeService) stopAndRemoveContainer(ctx context.Context, container moby.Container, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
w := progress.ContextWriter(ctx)
eventName := getContainerProgressName(container)
err := s.stopContainer(ctx, w, container, timeout)
err := s.stopContainer(ctx, w, service, container, timeout)
if errdefs.IsNotFound(err) {
w.Event(progress.RemovedEvent(eventName))
return nil

View File

@@ -96,6 +96,95 @@ func TestDown(t *testing.T) {
assert.NilError(t, err)
}
func TestDownWithGivenServices(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested := composeService{
dockerCli: cli,
}
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
[]moby.Container{
testContainer("service1", "123", false),
testContainer("service2", "456", false),
testContainer("service2", "789", false),
testContainer("service_orphan", "321", true),
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{}, nil)
// network names are not guaranteed to be unique, ensure Compose handles
// cleanup properly if duplicates are inadvertently created
api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]network.Summary{
{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
}, nil)
stopOptions := containerType.StopOptions{}
api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(nil)
api.EXPECT().ContainerRemove(gomock.Any(), "123", containerType.RemoveOptions{Force: true}).Return(nil)
api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{
Filters: filters.NewArgs(
projectFilter(strings.ToLower(testProject)),
networkFilter("default")),
}).Return([]network.Summary{
{ID: "abc123", Name: "myProject_default"},
}, nil)
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(network.Inspect{ID: "abc123"}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{
Services: []string{"service1", "not-running-service"},
})
assert.NilError(t, err)
}
func TestDownWithSpecifiedServiceButTheServicesAreNotRunning(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api, cli := prepareMocks(mockCtrl)
tested := composeService{
dockerCli: cli,
}
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
[]moby.Container{
testContainer("service1", "123", false),
testContainer("service2", "456", false),
testContainer("service2", "789", false),
testContainer("service_orphan", "321", true),
}, nil)
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{}, nil)
// network names are not guaranteed to be unique, ensure Compose handles
// cleanup properly if duplicates are inadvertently created
api.EXPECT().NetworkList(gomock.Any(), network.ListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]network.Summary{
{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
}, nil)
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{
Services: []string{"not-running-service1", "not-running-service2"},
})
assert.NilError(t, err)
}
func TestDownRemoveOrphans(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

101
pkg/compose/export.go Normal file
View File

@@ -0,0 +1,101 @@
/*
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 compose
import (
"context"
"fmt"
"io"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
)
func (s *composeService) Export(ctx context.Context, projectName string, options api.ExportOptions) error {
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
return s.export(ctx, projectName, options)
}, s.stdinfo(), "Exporting")
}
func (s *composeService) export(ctx context.Context, projectName string, options api.ExportOptions) error {
projectName = strings.ToLower(projectName)
container, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, options.Service, options.Index)
if err != nil {
return err
}
if options.Output == "" && s.dockerCli.Out().IsTerminal() {
return fmt.Errorf("output option is required when exporting to terminal")
}
if err := command.ValidateOutputPath(options.Output); err != nil {
return fmt.Errorf("failed to export container: %w", err)
}
clnt := s.dockerCli.Client()
w := progress.ContextWriter(ctx)
name := getCanonicalContainerName(container)
msg := fmt.Sprintf("export %s to %s", name, options.Output)
w.Event(progress.Event{
ID: name,
Text: msg,
Status: progress.Working,
StatusText: "Exporting",
})
responseBody, err := clnt.ContainerExport(ctx, container.ID)
if err != nil {
return err
}
defer func() {
if err := responseBody.Close(); err != nil {
w.Event(progress.Event{
ID: name,
Text: msg,
Status: progress.Error,
StatusText: fmt.Sprintf("Failed to close response body: %v", err),
})
}
}()
if !s.dryRun {
if options.Output == "" {
_, err := io.Copy(s.dockerCli.Out(), responseBody)
return err
}
if err := command.CopyToFile(options.Output, responseBody); err != nil {
return err
}
}
w.Event(progress.Event{
ID: name,
Text: msg,
Status: progress.Done,
StatusText: "Exported",
})
return nil
}

247
pkg/compose/generate.go Normal file
View File

@@ -0,0 +1,247 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
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/mount"
"github.com/docker/docker/api/types/network"
"golang.org/x/exp/maps"
)
func (s *composeService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
filtersListNames := filters.NewArgs()
filtersListIDs := filters.NewArgs()
for _, containerName := range options.Containers {
filtersListNames.Add("name", containerName)
filtersListIDs.Add("id", containerName)
}
containers, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
Filters: filtersListNames,
All: true,
})
if err != nil {
return nil, err
}
containersByIds, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
Filters: filtersListIDs,
All: true,
})
if err != nil {
return nil, err
}
for _, container := range containersByIds {
if !utils.Contains(containers, container) {
containers = append(containers, container)
}
}
if len(containers) == 0 {
return nil, fmt.Errorf("no container(s) found with the following name(s): %s", strings.Join(options.Containers, ","))
}
return s.createProjectFromContainers(containers, options.ProjectName)
}
func (s *composeService) createProjectFromContainers(containers []moby.Container, projectName string) (*types.Project, error) {
project := &types.Project{}
services := types.Services{}
networks := types.Networks{}
volumes := types.Volumes{}
secrets := types.Secrets{}
if projectName != "" {
project.Name = projectName
}
for _, c := range containers {
// if the container is from a previous Compose application, use the existing service name
serviceLabel, ok := c.Labels[api.ServiceLabel]
if !ok {
serviceLabel = getCanonicalContainerName(c)
}
service, ok := services[serviceLabel]
if !ok {
service = types.ServiceConfig{
Name: serviceLabel,
Image: c.Image,
Labels: c.Labels,
}
}
service.Scale = increment(service.Scale)
inspect, err := s.apiClient().ContainerInspect(context.Background(), c.ID)
if err != nil {
services[serviceLabel] = service
continue
}
s.extractComposeConfiguration(&service, inspect, volumes, secrets, networks)
service.Labels = cleanDockerPreviousLabels(service.Labels)
services[serviceLabel] = service
}
project.Services = services
project.Networks = networks
project.Volumes = volumes
project.Secrets = secrets
return project, nil
}
func (s *composeService) extractComposeConfiguration(service *types.ServiceConfig, inspect moby.ContainerJSON, volumes types.Volumes, secrets types.Secrets, networks types.Networks) {
service.Environment = types.NewMappingWithEquals(inspect.Config.Env)
if inspect.Config.Healthcheck != nil {
healthConfig := inspect.Config.Healthcheck
service.HealthCheck = s.toComposeHealthCheck(healthConfig)
}
if len(inspect.Mounts) > 0 {
detectedVolumes, volumeConfigs, detectedSecrets, secretsConfigs := s.toComposeVolumes(inspect.Mounts)
service.Volumes = append(service.Volumes, volumeConfigs...)
service.Secrets = append(service.Secrets, secretsConfigs...)
maps.Copy(volumes, detectedVolumes)
maps.Copy(secrets, detectedSecrets)
}
if len(inspect.NetworkSettings.Networks) > 0 {
detectedNetworks, networkConfigs := s.toComposeNetwork(inspect.NetworkSettings.Networks)
service.Networks = networkConfigs
maps.Copy(networks, detectedNetworks)
}
if len(inspect.HostConfig.PortBindings) > 0 {
for key, portBindings := range inspect.HostConfig.PortBindings {
for _, portBinding := range portBindings {
service.Ports = append(service.Ports, types.ServicePortConfig{
Target: uint32(key.Int()),
Published: portBinding.HostPort,
Protocol: key.Proto(),
HostIP: portBinding.HostIP,
})
}
}
}
}
func (s *composeService) toComposeHealthCheck(healthConfig *containerType.HealthConfig) *types.HealthCheckConfig {
var healthCheck types.HealthCheckConfig
healthCheck.Test = healthConfig.Test
if healthConfig.Timeout != 0 {
timeout := types.Duration(healthConfig.Timeout)
healthCheck.Timeout = &timeout
}
if healthConfig.Interval != 0 {
interval := types.Duration(healthConfig.Interval)
healthCheck.Interval = &interval
}
if healthConfig.StartPeriod != 0 {
startPeriod := types.Duration(healthConfig.StartPeriod)
healthCheck.StartPeriod = &startPeriod
}
if healthConfig.StartInterval != 0 {
startInterval := types.Duration(healthConfig.StartInterval)
healthCheck.StartInterval = &startInterval
}
if healthConfig.Retries != 0 {
retries := uint64(healthConfig.Retries)
healthCheck.Retries = &retries
}
return &healthCheck
}
func (s *composeService) toComposeVolumes(volumes []moby.MountPoint) (map[string]types.VolumeConfig,
[]types.ServiceVolumeConfig, map[string]types.SecretConfig, []types.ServiceSecretConfig) {
volumeConfigs := make(map[string]types.VolumeConfig)
secretConfigs := make(map[string]types.SecretConfig)
var serviceVolumeConfigs []types.ServiceVolumeConfig
var serviceSecretConfigs []types.ServiceSecretConfig
for _, volume := range volumes {
serviceVC := types.ServiceVolumeConfig{
Type: string(volume.Type),
Source: volume.Source,
Target: volume.Destination,
ReadOnly: !volume.RW,
}
switch volume.Type {
case mount.TypeVolume:
serviceVC.Source = volume.Name
vol := types.VolumeConfig{}
if volume.Driver != "local" {
vol.Driver = volume.Driver
vol.Name = volume.Name
}
volumeConfigs[volume.Name] = vol
serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
case mount.TypeBind:
if strings.HasPrefix(volume.Destination, "/run/secrets") {
destination := strings.Split(volume.Destination, "/")
secret := types.SecretConfig{
Name: destination[len(destination)-1],
File: strings.TrimPrefix(volume.Source, "/host_mnt"),
}
secretConfigs[secret.Name] = secret
serviceSecretConfigs = append(serviceSecretConfigs, types.ServiceSecretConfig{
Source: secret.Name,
Target: volume.Destination,
})
} else {
serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
}
}
}
return volumeConfigs, serviceVolumeConfigs, secretConfigs, serviceSecretConfigs
}
func (s *composeService) toComposeNetwork(networks map[string]*network.EndpointSettings) (map[string]types.NetworkConfig, map[string]*types.ServiceNetworkConfig) {
networkConfigs := make(map[string]types.NetworkConfig)
serviceNetworkConfigs := make(map[string]*types.ServiceNetworkConfig)
for name, net := range networks {
inspect, err := s.apiClient().NetworkInspect(context.Background(), name, network.InspectOptions{})
if err != nil {
networkConfigs[name] = types.NetworkConfig{}
} else {
networkConfigs[name] = types.NetworkConfig{
Internal: inspect.Internal,
}
}
serviceNetworkConfigs[name] = &types.ServiceNetworkConfig{
Aliases: net.Aliases,
}
}
return networkConfigs, serviceNetworkConfigs
}
func cleanDockerPreviousLabels(labels types.Labels) types.Labels {
cleanedLabels := types.Labels{}
for key, value := range labels {
if !strings.HasPrefix(key, "com.docker.compose.") && !strings.HasPrefix(key, "desktop.docker.io") {
cleanedLabels[key] = value
}
}
return cleanedLabels
}

View File

@@ -33,6 +33,7 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
o.Deploy.Replicas = nil
}
o.DependsOn = nil
o.Profiles = nil
bytes, err := json.Marshal(o)
if err != nil {

122
pkg/compose/hook.go Normal file
View File

@@ -0,0 +1,122 @@
/*
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 compose
import (
"context"
"fmt"
"io"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
moby "github.com/docker/docker/api/types"
containerType "github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
)
func (s composeService) runHook(ctx context.Context, container moby.Container, service types.ServiceConfig, hook types.ServiceHook, listener api.ContainerEventListener) error {
wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.HookEventLog,
Container: getContainerNameWithoutProject(container) + " ->",
ID: container.ID,
Service: service.Name,
Line: line,
})
})
defer wOut.Close() //nolint:errcheck
detached := listener == nil
exec, err := s.apiClient().ContainerExecCreate(ctx, container.ID, containerType.ExecOptions{
User: hook.User,
Privileged: hook.Privileged,
Env: ToMobyEnv(hook.Environment),
WorkingDir: hook.WorkingDir,
Cmd: hook.Command,
Detach: detached,
AttachStdout: !detached,
AttachStderr: !detached,
})
if err != nil {
return err
}
if detached {
return s.runWaitExec(ctx, exec, service, listener)
}
height, width := s.stdout().GetTtySize()
consoleSize := &[2]uint{height, width}
attach, err := s.apiClient().ContainerExecAttach(ctx, exec.ID, containerType.ExecAttachOptions{
Tty: service.Tty,
ConsoleSize: consoleSize,
})
if err != nil {
return err
}
defer attach.Close()
if service.Tty {
_, err = io.Copy(wOut, attach.Reader)
} else {
_, err = stdcopy.StdCopy(wOut, wOut, attach.Reader)
}
if err != nil {
return err
}
inspected, err := s.apiClient().ContainerExecInspect(ctx, exec.ID)
if err != nil {
return err
}
if inspected.ExitCode != 0 {
return fmt.Errorf("%s hook exited with status %d", service.Name, inspected.ExitCode)
}
return nil
}
func (s composeService) runWaitExec(ctx context.Context, exec moby.IDResponse, service types.ServiceConfig, listener api.ContainerEventListener) error {
err := s.apiClient().ContainerExecStart(ctx, exec.ID, containerType.ExecStartOptions{
Detach: listener == nil,
Tty: service.Tty,
})
if err != nil {
return nil
}
// We miss a ContainerExecWait API
tick := time.NewTicker(100 * time.Millisecond)
for {
select {
case <-ctx.Done():
return nil
case <-tick.C:
inspect, err := s.apiClient().ContainerExecInspect(ctx, exec.ID)
if err != nil {
return nil
}
if !inspect.Running {
if inspect.ExitCode != 0 {
return fmt.Errorf("%s hook exited with status %d", service.Name, inspect.ExitCode)
}
return nil
}
}
}
}

View File

@@ -98,7 +98,7 @@ func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() er
case api.UserCancel:
aborting = true
case api.ContainerEventAttach:
if _, ok := containers[id]; ok {
if attached, ok := containers[id]; ok && attached {
continue
}
containers[id] = true
@@ -148,7 +148,7 @@ func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() er
// Last container terminated, done
return exitCode, nil
}
case api.ContainerEventLog:
case api.ContainerEventLog, api.HookEventLog:
if !aborting {
p.consumer.Log(container, event.Line)
}

View File

@@ -35,7 +35,7 @@ func (s *composeService) Publish(ctx context.Context, project *types.Project, re
}
func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
err := s.Push(ctx, project, api.PushOptions{})
err := s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
if err != nil {
return err
}

View File

@@ -62,6 +62,9 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
w := progress.ContextWriter(ctx)
for _, service := range project.Services {
if service.Build == nil || service.Image == "" {
if options.ImageMandatory && service.Image == "" {
return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name)
}
w.Event(progress.Event{
ID: service.Name,
Status: progress.Done,

View File

@@ -93,7 +93,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
}
if !opts.NoDeps {
if err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, observedState); err != nil {
if err := s.waitDependencies(ctx, project, service.Name, service.DependsOn, observedState, 0); err != nil {
return "", err
}
}
@@ -109,7 +109,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
return "", err
}
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, createOpts)
created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
if err != nil {
return "", err
}

View File

@@ -129,7 +129,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
return err
}
return s.startService(ctx, project, service, containers)
return s.startService(ctx, project, service, containers, listener, options.WaitTimeout)
})
if err != nil {
return err
@@ -149,7 +149,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
defer cancel()
}
err = s.waitDependencies(ctx, project, project.Name, depends, containers)
err = s.waitDependencies(ctx, project, project.Name, depends, containers, 0)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fmt.Errorf("application not healthy after %s", options.WaitTimeout)
@@ -214,13 +214,13 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
}
var (
expected []string
expected = utils.NewSet[string]()
watched = map[string]int{}
replaced []string
)
for _, c := range containers {
if isRequired(c) {
expected = append(expected, c.ID)
expected.Add(c.ID)
}
watched[c.ID] = 0
}
@@ -242,7 +242,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
// be able to inspect in time before they're gone from the
// API, so just remove the watch without erroring
delete(watched, event.Container)
expected = utils.Remove(expected, event.Container)
expected.Remove(event.Container)
return nil
}
return err
@@ -253,7 +253,6 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
Labels: inspected.Config.Labels,
}
name := getContainerNameWithoutProject(container)
service := container.Labels[api.ServiceLabel]
switch event.Status {
case "stop":
@@ -278,7 +277,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
}
delete(watched, container.ID)
expected = utils.Remove(expected, container.ID)
expected.Remove(container.ID)
case "die":
restarted := watched[container.ID]
watched[container.ID] = restarted + 1
@@ -308,7 +307,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
if !willRestart {
// we're done with this one
delete(watched, container.ID)
expected = utils.Remove(expected, container.ID)
expected.Remove(container.ID)
}
case "start":
count, ok := watched[container.ID]
@@ -316,7 +315,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
if !ok {
// A new container has just been added to service by scale
watched[container.ID] = 0
expected = append(expected, container.ID)
expected.Add(container.ID)
mustAttach = true
}
if mustAttach {
@@ -333,17 +332,15 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
if err != nil {
return err
}
if utils.StringContains(expected, id) {
expected = append(expected, inspected.ID)
if expected.Has(id) {
expected.Add(inspected.ID)
expected.Add(container.ID)
}
watched[container.ID] = 1
if utils.Contains(expected, id) {
expected = append(expected, container.ID)
}
} else if ofInterest(container) {
watched[container.ID] = 1
if isRequired(container) {
expected = append(expected, container.ID)
expected.Add(container.ID)
}
}
}

View File

@@ -54,6 +54,7 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a
if !utils.StringContains(options.Services, service) {
return nil
}
return s.stopContainers(ctx, w, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout)
serv := project.Services[service]
return s.stopContainers(ctx, w, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout)
})
}

View File

@@ -98,10 +98,9 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
defer keyboard.Close() //nolint:errcheck
isWatchConfigured := s.shouldWatch(project)
isDockerDesktopActive := s.isDesktopIntegrationActive()
isDockerDesktopComposeUI := s.isDesktopUIEnabled()
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI)
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured)
formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI, signalChan, s.watch)
formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, signalChan, s.watch)
if options.Start.Watch {
formatter.KeyboardManager.StartWatch(ctx, doneCh, project, options)
}

View File

@@ -136,7 +136,7 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
service.PullPolicy = types.PullPolicyBuild
project.Services[i] = service
dockerIgnores, err := watch.LoadDockerIgnore(service.Build.Context)
dockerIgnores, err := watch.LoadDockerIgnore(service.Build)
if err != nil {
return err
}
@@ -294,7 +294,7 @@ func maybeFileEvent(trigger types.Trigger, hostPath string, ignore watch.PathMat
return nil
}
// always use Unix-style paths for inside the container
containerPath = path.Join(trigger.Target, rel)
containerPath = path.Join(trigger.Target, filepath.ToSlash(rel))
}
return &fileEvent{
@@ -512,9 +512,15 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
return err
}
services := []string{serviceName}
p, err := project.WithSelectedServices(services)
if err != nil {
return err
}
err = s.start(ctx, project.Name, api.StartOptions{
Project: project,
Services: []string{serviceName},
Project: p,
Services: services,
AttachTo: services,
}, nil)
if err != nil {
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Application failed to start after update. Error: %v", err))
@@ -527,7 +533,7 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
pathMappings[i] = batch[i].PathMapping
}
writeWatchSyncMessage(options.LogTo, serviceName, pathMappings)
writeWatchSyncMessage(options.LogTo, serviceName, pathMappings, restartService)
service, err := project.GetService(serviceName)
if err != nil {
@@ -537,17 +543,28 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
return err
}
if restartService {
return s.Restart(ctx, project.Name, api.RestartOptions{
err = s.restart(ctx, project.Name, api.RestartOptions{
Services: []string{serviceName},
Project: project,
NoDeps: false,
})
if err != nil {
return err
}
options.LogTo.Log(
api.WatchLogger,
fmt.Sprintf("service %q restarted", serviceName))
}
return nil
}
// writeWatchSyncMessage prints out a message about the sync for the changed paths.
func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings []sync.PathMapping) {
func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings []sync.PathMapping, restart bool) {
action := "Syncing"
if restart {
action = "Syncing and restarting"
}
if logrus.IsLevelEnabled(logrus.DebugLevel) {
hostPathsToSync := make([]string, len(pathMappings))
for i := range pathMappings {
@@ -556,7 +573,8 @@ func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings
log.Log(
api.WatchLogger,
fmt.Sprintf(
"Syncing %q after changes were detected: %s",
"%s service %q after changes were detected: %s",
action,
serviceName,
strings.Join(hostPathsToSync, ", "),
),
@@ -564,7 +582,7 @@ func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings
} else {
log.Log(
api.WatchLogger,
fmt.Sprintf("Syncing service %q after %d changes were detected", serviceName, len(pathMappings)),
fmt.Sprintf("%s service %q after %d changes were detected", action, serviceName, len(pathMappings)),
)
}
}

View File

@@ -65,3 +65,32 @@ func TestLocalComposeExec(t *testing.T) {
assert.Check(t, !strings.Contains(res.Stdout(), "FOO="), res.Combined())
})
}
func TestLocalComposeExecOneOff(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "compose-e2e-exec-one-off"
cmdArgs := func(cmd string, args ...string) []string {
ret := []string{"--project-directory", "fixtures/simple-composefile", "--project-name", projectName, cmd}
ret = append(ret, args...)
return ret
}
cleanup := func() {
c.RunDockerComposeCmd(t, cmdArgs("down", "--timeout=0")...)
}
cleanup()
t.Cleanup(cleanup)
c.RunDockerComposeCmd(t, cmdArgs("run", "-d", "simple")...)
t.Run("exec in one-off container", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, cmdArgs("exec", "-e", "FOO", "simple", "/usr/bin/env")...)
assert.Check(t, !strings.Contains(res.Stdout(), "FOO="), res.Combined())
})
t.Run("exec with index", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, cmdArgs("exec", "--index", "1", "-e", "FOO", "simple", "/usr/bin/env")...)
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "service \"simple\" is not running container #1"})
})
}

View File

@@ -62,7 +62,6 @@ func TestLocalComposeRun(t *testing.T) {
assert.Assert(t, runContainerID != "")
res = c.RunDockerCmd(t, "inspect", runContainerID)
res.Assert(t, icmd.Expected{Out: ` "Status": "exited"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.container-number": "1"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.project": "run-test"`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.oneoff": "True",`})
res.Assert(t, icmd.Expected{Out: `"com.docker.compose.slug": "` + truncatedSlug})

31
pkg/e2e/env_file_test.go Normal file
View File

@@ -0,0 +1,31 @@
/*
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 e2e
import (
"strings"
"testing"
"gotest.tools/v3/assert"
)
func TestRawEnvFile(t *testing.T) {
c := NewParallelCLI(t)
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dotenv/raw.yaml", "run", "test")
assert.Equal(t, strings.TrimSpace(res.Stdout()), "'{\"key\": \"value\"}'")
}

50
pkg/e2e/export_test.go Normal file
View File

@@ -0,0 +1,50 @@
/*
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 e2e
import (
"testing"
)
func TestExport(t *testing.T) {
const projectName = "e2e-export-service"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service")
c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "service.tar", "service")
}
func TestExportWithReplicas(t *testing.T) {
const projectName = "e2e-export-service-with-replicas"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/export/compose.yaml", "--project-name", projectName, "up", "-d", "service-with-replicas")
c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r1.tar", "--index=1", "service-with-replicas")
c.RunDockerComposeCmd(t, "--project-name", projectName, "export", "-o", "r2.tar", "--index=2", "service-with-replicas")
}

View File

@@ -0,0 +1 @@
TEST_VAR='{"key": "value"}'

View File

@@ -0,0 +1,7 @@
services:
test:
image: alpine
command: sh -c "echo $$TEST_VAR"
env_file:
- path: .env.raw
format: raw # parse without interpolation

View File

@@ -0,0 +1,9 @@
services:
service:
image: alpine
command: sleep infinity
service-with-replicas:
image: alpine
command: sleep infinity
deploy:
replicas: 3

View File

@@ -0,0 +1,15 @@
services:
foo:
container_name: foo_c
profiles: [ test ]
image: alpine
depends_on: [ db ]
bar:
container_name: bar_c
profiles: [ test ]
image: alpine
db:
container_name: db_c
image: alpine

View File

@@ -179,3 +179,16 @@ func TestUpWithAllResources(t *testing.T) {
assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Volume "%s_my_vol" Created`, projectName)), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Network %s_my_net Created`, projectName)), res.Combined())
}
func TestUpProfile(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-up-profile"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "--profile", "test", "down", "-v")
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/docker-compose.yaml", "--project-name", projectName, "up", "foo")
assert.Assert(t, strings.Contains(res.Combined(), `Container db_c Created`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `Container foo_c Created`), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), `Container bar_c Created`), res.Combined())
}

View File

@@ -99,9 +99,8 @@ func TestRebuildOnDotEnvWithExternalNetwork(t *testing.T) {
errors)
}, 30*time.Second, 1*time.Second)
n := c.RunDockerCmd(t, "network", "inspect", networkName, "-f", "{{ .Id }}")
pn := c.RunDockerCmd(t, "inspect", containerName, "-f", "{{ .HostConfig.NetworkMode }}")
assert.Equal(t, pn.Stdout(), n.Stdout())
assert.Equal(t, strings.TrimSpace(pn.Stdout()), networkName)
t.Log("create a dotenv file that will be used to trigger the rebuild")
err = os.WriteFile(dotEnvFilepath, []byte("HELLO=WORLD\nTEST=REBUILD"), 0o666)
@@ -119,9 +118,8 @@ func TestRebuildOnDotEnvWithExternalNetwork(t *testing.T) {
return true, fmt.Sprintf("container %s was rebuilt", containerName)
}, 30*time.Second, 1*time.Second)
n2 := c.RunDockerCmd(t, "network", "inspect", networkName, "-f", "{{ .Id }}")
pn2 := c.RunDockerCmd(t, "inspect", containerName, "-f", "{{ .HostConfig.NetworkMode }}")
assert.Equal(t, pn2.Stdout(), n2.Stdout())
assert.Equal(t, strings.TrimSpace(pn2.Stdout()), networkName)
assert.Check(t, !strings.Contains(r.Combined(), "Application failed to start after update"))
@@ -290,8 +288,7 @@ func doTest(t *testing.T, svcName string) {
return poll.Continue("%v", r.Combined())
}
}
poll.WaitOn(t, checkRestart(fmt.Sprintf("%s-1 Restarting", svcName)))
poll.WaitOn(t, checkRestart(fmt.Sprintf("%s-1 Started", svcName)))
poll.WaitOn(t, checkRestart(fmt.Sprintf("service %q restarted", svcName)))
poll.WaitOn(t, checkFileContents("/app/config/file.config", "This is an updated config file"))
testComplete.Store(true)

View File

@@ -155,6 +155,35 @@ func (mr *MockServiceMockRecorder) Exec(ctx, projectName, options any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockService)(nil).Exec), ctx, projectName, options)
}
// Export mocks base method.
func (m *MockService) Export(ctx context.Context, projectName string, options api.ExportOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Export", ctx, projectName, options)
ret0, _ := ret[0].(error)
return ret0
}
// Export indicates an expected call of Export.
func (mr *MockServiceMockRecorder) Export(ctx, projectName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Export", reflect.TypeOf((*MockService)(nil).Export), ctx, projectName, options)
}
// Generate mocks base method.
func (m *MockService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Generate", ctx, options)
ret0, _ := ret[0].(*types.Project)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Generate indicates an expected call of Generate.
func (mr *MockServiceMockRecorder) Generate(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockService)(nil).Generate), ctx, options)
}
// Images mocks base method.
func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
m.ctrl.T.Helper()

View File

@@ -30,6 +30,7 @@ import (
"github.com/docker/buildx/store/storeutil"
"github.com/docker/buildx/util/imagetools"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/ocipush"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
@@ -113,6 +114,8 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
err2 := g.pullComposeFiles(ctx, local, composeFile, manifest, ref, resolver)
if err2 != nil {
// we need to clean up the directory to be sure we won't let empty files present
_ = os.RemoveAll(local)
return "", err2
}
}
@@ -137,8 +140,8 @@ func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, com
return err
}
defer f.Close() //nolint:errcheck
if manifest.ArtifactType != "application/vnd.docker.compose.project" {
if (manifest.ArtifactType != "" && manifest.ArtifactType != ocipush.ComposeProjectArtifactType) ||
(manifest.ArtifactType == "" && manifest.Config.MediaType != ocipush.ComposeEmptyConfigMediaType) {
return fmt.Errorf("%s is not a compose project OCI artifact, but %s", ref.String(), manifest.ArtifactType)
}

View File

@@ -18,10 +18,13 @@ package watch
import (
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/paths"
"github.com/moby/patternmatcher"
"github.com/moby/patternmatcher/ignorefile"
@@ -61,13 +64,28 @@ func (i dockerPathMatcher) MatchesEntireDir(f string) (bool, error) {
return true, nil
}
func LoadDockerIgnore(repoRoot string) (*dockerPathMatcher, error) {
func LoadDockerIgnore(build *types.BuildConfig) (*dockerPathMatcher, error) {
repoRoot := build.Context
absRoot, err := filepath.Abs(repoRoot)
if err != nil {
return nil, err
}
patterns, err := readDockerignorePatterns(absRoot)
// first try Dockerfile-specific ignore-file
f, err := os.Open(filepath.Join(repoRoot, build.Dockerfile+".dockerignore"))
if os.IsNotExist(err) {
// defaults to a global .dockerignore
f, err = os.Open(filepath.Join(repoRoot, ".dockerignore"))
if os.IsNotExist(err) {
return NewDockerPatternMatcher(repoRoot, nil)
}
}
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
patterns, err := readDockerignorePatterns(f)
if err != nil {
return nil, err
}
@@ -111,6 +129,15 @@ func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMat
return nil, err
}
// Check if "*" is present in patterns
hasAllPattern := slices.Contains(patterns, "*")
if hasAllPattern {
// Remove all non-exclusion patterns (those that don't start with '!')
patterns = slices.DeleteFunc(patterns, func(p string) bool {
return p != "" && p[0] != '!' // Only keep exclusion patterns
})
}
pm, err := patternmatcher.New(absPatterns(absRoot, patterns))
if err != nil {
return nil, err
@@ -122,19 +149,8 @@ func NewDockerPatternMatcher(repoRoot string, patterns []string) (*dockerPathMat
}, nil
}
func readDockerignorePatterns(repoRoot string) ([]string, error) {
var excludes []string
f, err := os.Open(filepath.Join(repoRoot, ".dockerignore"))
switch {
case os.IsNotExist(err):
return excludes, nil
case err != nil:
return nil, err
}
defer func() { _ = f.Close() }()
patterns, err := ignorefile.ReadAll(f)
func readDockerignorePatterns(r io.Reader) ([]string, error) {
patterns, err := ignorefile.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("error reading .dockerignore: %w", err)
}

View File

@@ -0,0 +1,108 @@
/*
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 watch
import (
"testing"
)
func TestNewDockerPatternMatcher(t *testing.T) {
tests := []struct {
name string
repoRoot string
patterns []string
expectedErr bool
expectedRoot string
expectedPat []string
}{
{
name: "Basic patterns without wildcard",
repoRoot: "/repo",
patterns: []string{"dir1/", "file.txt"},
expectedErr: false,
expectedRoot: "/repo",
expectedPat: []string{"/repo/dir1", "/repo/file.txt"},
},
{
name: "Patterns with exclusion",
repoRoot: "/repo",
patterns: []string{"dir1/", "!file.txt"},
expectedErr: false,
expectedRoot: "/repo",
expectedPat: []string{"/repo/dir1", "!/repo/file.txt"},
},
{
name: "Wildcard with exclusion",
repoRoot: "/repo",
patterns: []string{"*", "!file.txt"},
expectedErr: false,
expectedRoot: "/repo",
expectedPat: []string{"!/repo/file.txt"},
},
{
name: "No patterns",
repoRoot: "/repo",
patterns: []string{},
expectedErr: false,
expectedRoot: "/repo",
expectedPat: nil,
},
{
name: "Only exclusion pattern",
repoRoot: "/repo",
patterns: []string{"!file.txt"},
expectedErr: false,
expectedRoot: "/repo",
expectedPat: []string{"!/repo/file.txt"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Call the function with the test data
matcher, err := NewDockerPatternMatcher(tt.repoRoot, tt.patterns)
// Check if we expect an error
if (err != nil) != tt.expectedErr {
t.Fatalf("expected error: %v, got: %v", tt.expectedErr, err)
}
// If no error is expected, check the output
if !tt.expectedErr {
if matcher.repoRoot != tt.expectedRoot {
t.Errorf("expected root: %v, got: %v", tt.expectedRoot, matcher.repoRoot)
}
// Compare patterns
actualPatterns := matcher.matcher.Patterns()
if len(actualPatterns) != len(tt.expectedPat) {
t.Errorf("expected patterns length: %v, got: %v", len(tt.expectedPat), len(actualPatterns))
}
for i, expectedPat := range tt.expectedPat {
actualPatternStr := actualPatterns[i].String()
if actualPatterns[i].Exclusion() {
actualPatternStr = "!" + actualPatternStr
}
if actualPatternStr != expectedPat {
t.Errorf("expected pattern: %v, got: %v", expectedPat, actualPatterns[i])
}
}
}
})
}
}