Compare commits

...

55 Commits

Author SHA1 Message Date
Guillaume Lours
8318f66330 Merge pull request #10791 from milas/watch-refactor-sync
watch: move sync logic into separate package
2023-07-19 13:11:24 +02:00
Milas Bowman
cb17c3c8a6 watch: move sync logic into separate package
Just moving some code around in preparation for an alternative
sync implementation that can do bulk transfers by using `tar`.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-19 12:25:13 +02:00
Guillaume Lours
9174a99d27 Merge pull request #10828 from thaJeztah/minor_cli_changes
pkg/compose: RunOneOffContainer: don't use NewStartOptions()
2023-07-19 12:24:37 +02:00
Sebastiaan van Stijn
4eb43c53fa pkg/compose: RunOneOffContainer: don't use NewStartOptions()
It's no longer used in docker/cli, and doesn't do anything other than
creating an empty struct, so replacing it (as we're planning to
deprecate that function)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-07-19 12:13:40 +02:00
Guillaume Lours
150b88ab5d Merge pull request #10829 from milas/e2e-watch-test-fix
test: watch e2e reliability tweaks
2023-07-19 12:07:10 +02:00
Guillaume Lours
5159058c7e Merge pull request #10831 from milas/instrument-up
trace: instrument `compose up` at a high-level
2023-07-19 12:06:56 +02:00
Milas Bowman
1ae191a936 trace: instrument compose up at a high-level
* Image pull
* Image build
* Service apply
  * Scale down/up (event)
  * Recreate container (event)
  * Scale up (event)
  * Container start (event)

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-19 11:26:12 +02:00
Guillaume Lours
3b2f3cdce3 Merge pull request #10819 from ndeloof/windows_abs
check secret target is an absolute windows path
2023-07-19 11:24:45 +02:00
Nicolas De Loof
47778f8b77 check secret target is an absolute windows path
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-19 10:57:22 +02:00
Guillaume Lours
7d88edaf24 Merge pull request #10814 from milas/fix-build-push
build: do not attempt to push unnamed service images
2023-07-19 10:57:00 +02:00
Milas Bowman
636c13f818 build: do not attempt to push unnamed service images
When building, if images are being pushed, ensure that only
named images (i.e. services with a populated `image` field)
are attempted to be pushed.

Services without `image` get an auto-generated name, which
will be a "Docker library" reference since they're in the
format `$project-$service`, which is implicitly the same as
`docker.io/library/$project-$service`. A push for that is
never desirable / will always fail.

The key here is that we cannot overwrite the `<svc>.image`
field when doing builds, as we need to be able to check for
its presence to determine whether a push makes sense.

Fixes #10813.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-19 09:58:37 +02:00
Guillaume Lours
5a072b1ad5 Merge pull request #10792 from glours/add-depends_on-required
add support of depends_on.required attribute
2023-07-19 09:53:49 +02:00
Milas Bowman
ddceb1ac9d test: do not run watch e2e tests in parallel
This isn't playing nicely with the GHA CI runner.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-18 18:54:23 -04:00
Milas Bowman
d48f28c72c test: skip watch e2e test on macOS for the moment
Fix forthcoming via https://github.com/compose-spec/compose-go/pull/436
which addresses some symlink limitations. These can
actually effect other platforms but are most common
on macOS because the test creates temporary directories,
which are symlinked on macOS.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-18 18:53:26 -04:00
Guillaume Lours
2d16a05afa only check if a dependency is required when something unexpected happens
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-07-18 23:45:31 +02:00
Guillaume Lours
bb94ea034e add support of depends_on.required attribute
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-07-18 23:13:47 +02:00
Milas Bowman
0938c7e96f Merge pull request #10827 from thaJeztah/bump_buildx_buildkit
go.mod: buildx v0.11.2, buildkit v0.12, docker/cli v24.0.5-dev
2023-07-18 16:43:24 -04:00
Sebastiaan van Stijn
f429ee958a go.mod: github.com/docker/docker v24.0.5-dev (tip of 24 release branch)
full diff: 8443a06149...f329397077

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-07-18 22:25:50 +02:00
Sebastiaan van Stijn
e9ded2c518 go.mod: github.com/docker/buildx v0.11.2
full diff:

- https://github.com/docker/buildx/compare/v0.11.1...v0.11.2
- https://github.com/moby/buildkit/v0.12.0...faa0cc7da353

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-07-18 21:57:42 +02:00
Sebastiaan van Stijn
54e6e0bd8f go.mod: github.com/moby/buildkit v0.12.0
Switching to back to released versions / release-branche. The old version
was a commit from master (v0.12.0-dev).

full diff:

- https://github.com/moby/buildkit/compare/2d91ddcceedc...v0.12.0
- https://github.com/tonistiigi/fsutil/compare/9e7a6df48576...36ef4d8c0dbb

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-07-18 21:54:49 +02:00
Milas Bowman
3bc871e64b test: speed up the e2e test suite
Lots of our phony Compose files launch pointless long-lived processes
so we can assert on state. However, this means they often don't respond
well to signals on their own, requiring Compose to timeout and kill
them when doing a `down`.

Add in lots of `init: true` where appropriate so that we don't block
for no reason while running E2E tests all over the place.

Additionally, a couple tests have gotten a cleanup so they don't leave
behind containers. I still want to build this into the framework in
the future, but this is easier for the moment and won't cause any
trouble in the future.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-18 11:08:06 +02:00
Milas Bowman
6ff15d9472 Merge pull request #10812 from thaJeztah/update_go_1.20.6
update go to go1.20.6
2023-07-17 12:08:05 -04:00
Sebastiaan van Stijn
49bc0603e3 update go to go1.20.6
go1.20.6 (released 2023-07-11) includes a security fix to the net/http package,
as well as bug fixes to the compiler, cgo, the cover tool, the go command,
the runtime, and the crypto/ecdsa, go/build, go/printer, net/mail, and text/template
packages. See the Go 1.20.6 milestone on our issue tracker for details.

https://github.com/golang/go/issues?q=milestone%3AGo1.20.6+label%3ACherryPickApproved

Full diff: https://github.com/golang/go/compare/go1.20.5...go1.20.6

These minor releases include 1 security fixes following the security policy:

net/http: insufficient sanitization of Host header

The HTTP/1 client did not fully validate the contents of the Host header.
A maliciously crafted Host header could inject additional headers or entire
requests. The HTTP/1 client now refuses to send requests containing an
invalid Request.Host or Request.URL.Host value.

Thanks to Bartek Nowotarski for reporting this issue.

Includes security fixes for [CVE-2023-29406 ][1] and Go issue https://go.dev/issue/60374

[1]: https://github.com/advisories/GHSA-f8f7-69v5-w4vx

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-07-17 17:35:48 +02:00
Sebastiaan van Stijn
ce8a09b53f go.mod: github.com/docker/docker 8443a06149b5 (v24.0.5-dev) (#10810)
relevant changes:

- client: define a "dummy" hostname to use for local connections
  fixes "http: invalid Host header" errors when compiling with
  go1.20.6 or go1.19.11

full diff: https://github.com/docker/docker/compare/v24.0.4...8443a06149b5ba9c0763b92f832698474bcf2a13

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-07-17 10:47:52 -04:00
Milas Bowman
3dc8734897 watch: add end-to-end test (#10801)
Add an end-to-end test that covers the core watch functionality,
i.e. CRUD on files & directories.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-17 10:47:36 -04:00
Guillaume Lours
852e192820 bump buildkit to version v0.11.0-rc3.0.20230620112432-2d91ddcceedc (#10794)
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-07-12 08:23:02 -04:00
dependabot[bot]
d9e7859664 build(deps): bump github.com/docker/cli from 24.0.2+incompatible to 24.0.4+incompatible (#10799)
build(deps): bump github.com/docker/cli

Bumps [github.com/docker/cli](https://github.com/docker/cli) from 24.0.2+incompatible to 24.0.4+incompatible.
- [Commits](https://github.com/docker/cli/compare/v24.0.2...v24.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 08:21:46 -04:00
Guillaume Lours
e28b223650 Merge pull request #10793 from milas/dockerfile-cache-mounts
ci: speed up a couple Dockerfile targets w/ cache mount
2023-07-10 19:26:41 +02:00
Milas Bowman
1964693074 ci: speed up a couple Dockerfile targets w/ cache mount
The local Go package module path was missing from a couple of jobs,
which made them slower than needed since they were re-downloading
a bunch of dependencies.

In particular, this makes `make lint` waaaay faster!

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-10 13:13:10 -04:00
Guillaume Lours
dc74e6aa0e Merge pull request #10776 from docker/dependabot/go_modules/github.com/docker/buildx-0.11.1
build(deps): bump github.com/docker/buildx from 0.11.0 to 0.11.1
2023-07-10 18:15:39 +02:00
dependabot[bot]
b182cf6850 build(deps): bump github.com/docker/buildx from 0.11.0 to 0.11.1
Bumps [github.com/docker/buildx](https://github.com/docker/buildx) from 0.11.0 to 0.11.1.
- [Release notes](https://github.com/docker/buildx/releases)
- [Commits](https://github.com/docker/buildx/compare/v0.11.0...v0.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 15:51:31 +00:00
Guillaume Lours
f330b24632 Merge pull request #10788 from docker/dependabot/go_modules/github.com/docker/docker-24.0.4incompatible
build(deps): bump github.com/docker/docker from 24.0.2+incompatible to 24.0.4+incompatible
2023-07-10 17:49:45 +02:00
Guillaume Lours
8339269e13 Merge pull request #10789 from ndeloof/run_no_deps
Apply no-deps before we select and mutate target service
2023-07-10 15:46:01 +02:00
Guillaume Lours
ee6aeed84e Merge pull request #10700 from ndeloof/attach
support `attach`
2023-07-10 15:17:44 +02:00
Guillaume Lours
7a9dfa4284 Merge pull request #10790 from milas/e2e-process-leak
test: fix process leak in wait e2e test
2023-07-10 15:09:41 +02:00
Guillaume Lours
29daae3d6e Merge pull request #10784 from shantanoo-desai/v2
fix(secrets): file permission value does not comply with spec
2023-07-10 14:54:16 +02:00
Milas Bowman
8dea7b5cae test: fix process leak in wait e2e test
* Run `down` before and after test to not leave around containers
* Kill the `wait` process that's waiting on `infinity`
  * NOTE: If the test is actually working, this should exit once
    the `down` happens, but this ensures that we kill everything
    we start

I'd like to generalize more of this into the framework, but this
is a quick fix to prevent filling up CI machines with tons of
processes over time.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-10 08:42:09 -04:00
Guillaume Lours
bc6ad2e4a4 Merge pull request #10764 from docker/dependabot/go_modules/github.com/opencontainers/image-spec-1.1.0-rc4
build(deps): bump github.com/opencontainers/image-spec from 1.1.0-rc3 to 1.1.0-rc4
2023-07-10 14:39:44 +02:00
Nicolas De Loof
e6a7694b8d Apply no-deps before we select and mutate target service
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-10 14:37:42 +02:00
Nicolas De Loof
46d936c750 support attach
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-10 14:34:28 +02:00
dependabot[bot]
15bc7850bb build(deps): bump github.com/opencontainers/image-spec
Bumps [github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec) from 1.1.0-rc3 to 1.1.0-rc4.
- [Release notes](https://github.com/opencontainers/image-spec/releases)
- [Changelog](https://github.com/opencontainers/image-spec/blob/main/RELEASES.md)
- [Commits](https://github.com/opencontainers/image-spec/compare/v1.1.0-rc3...v1.1.0-rc4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 14:17:14 +02:00
Guillaume Lours
8a64ab56a0 Merge pull request #10760 from docker/dependabot/go_modules/gotest.tools/v3-3.5.0
build(deps): bump gotest.tools/v3 from 3.4.0 to 3.5.0
2023-07-10 14:16:34 +02:00
dependabot[bot]
1178c51e6a build(deps): bump gotest.tools/v3 from 3.4.0 to 3.5.0
Bumps [gotest.tools/v3](https://github.com/gotestyourself/gotest.tools) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/gotestyourself/gotest.tools/releases)
- [Commits](https://github.com/gotestyourself/gotest.tools/compare/v3.4.0...v3.5.0)

---
updated-dependencies:
- dependency-name: gotest.tools/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 12:40:37 +02:00
dependabot[bot]
3b3fd3e56c build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.2+incompatible to 24.0.4+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v24.0.2...v24.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 10:39:23 +00:00
Guillaume Lours
b1e10f559e Merge pull request #10781 from milas/deps-docs-0.6
deps: bump docker/cli-docs-tool to v0.6.0
2023-07-10 12:38:04 +02:00
Milas Bowman
baea5a48f5 deps: bump docker/cli-docs-tool to v0.6.0
Required re-running `make docs` to pick up changes.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-10 12:23:48 +02:00
Guillaume Lours
cb3a6ce52b Merge pull request #10787 from docker/dependabot/go_modules/google.golang.org/grpc-1.56.2
build(deps): bump google.golang.org/grpc from 1.56.0 to 1.56.2
2023-07-10 12:23:25 +02:00
dependabot[bot]
28f3802a07 build(deps): bump google.golang.org/grpc from 1.56.0 to 1.56.2
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.56.0 to 1.56.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.56.0...v1.56.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 09:23:22 +00:00
Shan Desai
fd0e0a2cbd fix(secrets): file permission value does not comply with spec
closes #10783

Compose Spec mentions that default values for secrets is `0444` aka. world-readable permissions. However, the value was previously set to `0400`. 


Signed-off-by: Shan Desai <shantanoo.desai@gmail.com>
2023-07-07 18:58:21 +02:00
Guillaume Lours
e90df62bb0 Merge pull request #10763 from ndeloof/exec_index
when --index is not set select first service container
2023-07-07 14:39:50 +02:00
Nicolas De Loof
b0af2deb2b when --index is not set select first service container
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-07 14:08:24 +02:00
Milas Bowman
be22bc735a network: fix random missing network when service has more than one
As part of the fix for #10668, the logic was adjusted so that the
default (highest-priority) network is used in the `ContainerCreate`,
and then the remaining networks are connected via calls to
`NetworkConnect` before starting the container.

Unfortunately, `ServiceConfig::NetworksByPriority` is neither
deterministic nor stable when networks have the same priority.

It's non-deterministic because the order of networks from parsing
YAML is random, since they are loaded into a Go map (which have
random iteration order). Additionally, it's not using a `SortStable`
in `compose-go`, so even if the load order was predictable, it
still might produce different results.

While I look at improving `compose-go` here to prevent this from
tripping us up in the future, this fix looks at _all_ networks for
a service and ignores the "default" one now. Before, it would
always skip the first one in the slice since that _should_ have
been the "default".

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-07-07 09:18:01 +02:00
Nicolas De Loof
b5f5e27597 don't use unitialized cli to setup DryRunClient
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-06 14:27:57 +02:00
Guillaume Lours
b1334b8dfc Merge pull request #10768 from cloud-native-team/v2
fix some comments
2023-07-06 10:06:05 +02:00
cui fliter
25ca75db4d fix some comments
Signed-off-by: cui fliter <imcusg@gmail.com>
2023-07-04 11:34:49 +08:00
96 changed files with 1119 additions and 302 deletions

View File

@@ -15,7 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
ARG GO_VERSION=1.20.5
ARG GO_VERSION=1.20.6
ARG XX_VERSION=1.2.1
ARG GOLANGCI_LINT_VERSION=v1.53.2
ARG ADDLICENSE_VERSION=v1.0.0
@@ -91,6 +91,7 @@ FROM build-base AS lint
ARG BUILD_TAGS
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
--mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
golangci-lint run --build-tags "$BUILD_TAGS" ./...
@@ -129,6 +130,7 @@ FROM base AS docsgen
WORKDIR /src
RUN --mount=target=. \
--mount=target=/root/.cache,type=cache \
--mount=type=cache,target=/go/pkg/mod \
go build -o /out/docsgen ./docs/yaml/main/generate.go
FROM --platform=${BUILDPLATFORM} alpine AS docs-build

View File

@@ -26,6 +26,7 @@ import (
"strings"
"syscall"
"github.com/compose-spec/compose-go/dotenv"
buildx "github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
@@ -472,7 +473,7 @@ func setEnvWithDotEnv(prjOpts *ProjectOptions) error {
return err
}
envFromFile, err := cli.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), workingDir, options.EnvFiles)
envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), workingDir, options.EnvFiles)
if err != nil {
return err
}

View File

@@ -233,11 +233,7 @@ func runConfigImages(streams api.Streams, opts configOptions, services []string)
return err
}
for _, s := range project.Services {
if s.Image != "" {
fmt.Fprintln(streams.Out(), s.Image)
} else {
fmt.Fprintf(streams.Out(), "%s%s%s\n", project.Name, api.Separator, s.Name)
}
fmt.Fprintln(streams.Out(), api.GetImageNameOrDefault(s, project.Name))
}
return nil
}

View File

@@ -64,10 +64,10 @@ func copyCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
}
flags := copyCmd.Flags()
flags.IntVar(&opts.index, "index", 0, "Index of the container if there are multiple instances of a service .")
flags.BoolVar(&opts.all, "all", false, "Copy to all the containers of the service.")
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.MarkDeprecated("all", "by default all the containers of the service will get the source file/directory to be copied.") //nolint:errcheck
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")

View File

@@ -65,7 +65,7 @@ func execCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *c
runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background.")
runCmd.Flags().StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
runCmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if there are multiple instances of a service [default: 1].")
runCmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas")
runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process.")
runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user.")
runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")

View File

@@ -57,7 +57,7 @@ func portCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *c
ValidArgsFunction: completeServiceNames(p),
}
cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp")
cmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if service has multiple replicas")
cmd.Flags().IntVar(&opts.index, "index", 0, "index of the container if service has multiple replicas")
return cmd
}

View File

@@ -63,6 +63,13 @@ type runOptions struct {
}
func (options runOptions) apply(project *types.Project) error {
if options.noDeps {
err := project.ForServices([]string{options.Service}, types.IgnoreDependencies)
if err != nil {
return err
}
}
target, err := project.GetService(options.Service)
if err != nil {
return err
@@ -93,13 +100,6 @@ func (options runOptions) apply(project *types.Project) error {
}
}
if options.noDeps {
err := project.ForServices([]string{options.Service}, types.IgnoreDependencies)
if err != nil {
return err
}
}
for i, s := range project.Services {
if s.Name == options.Service {
project.Services[i] = target

View File

@@ -21,9 +21,8 @@ import (
"fmt"
"time"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
@@ -164,17 +163,31 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
consumer = formatter.NewLogConsumer(ctx, streams.Out(), streams.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)
}
attachTo := services
attachTo := utils.Set[string]{}
if len(upOptions.attach) > 0 {
attachTo = upOptions.attach
attachTo.AddAll(upOptions.attach...)
}
if upOptions.attachDependencies {
attachTo = project.ServiceNames()
if err := project.WithServices(attachTo.Elements(), func(s types.ServiceConfig) error {
if s.Attach == nil || *s.Attach {
attachTo.Add(s.Name)
}
return nil
}); err != nil {
return err
}
}
if len(attachTo) == 0 {
attachTo = project.ServiceNames()
if err := project.WithServices(services, func(s types.ServiceConfig) error {
if s.Attach == nil || *s.Attach {
attachTo.Add(s.Name)
}
return nil
}); err != nil {
return err
}
}
attachTo = utils.Remove(attachTo, upOptions.noAttach...)
attachTo.RemoveAll(upOptions.noAttach...)
create := api.CreateOptions{
Services: services,
@@ -198,7 +211,7 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
Start: api.StartOptions{
Project: project,
Attach: consumer,
AttachTo: attachTo,
AttachTo: attachTo.Elements(),
ExitCodeFrom: upOptions.exitCodeFrom,
CascadeStop: upOptions.cascadeStop,
Wait: upOptions.wait,

View File

@@ -7,7 +7,6 @@ Define and run multi-container applications with Docker.
| Name | Description |
|:--------------------------------|:------------------------------------------------------------------------|
| [`alpha`](compose_alpha.md) | Experimental commands |
| [`build`](compose_build.md) | Build or rebuild services |
| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format |
| [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem |

View File

@@ -5,12 +5,12 @@ Copy files/folders between a service container and the local filesystem
### Options
| Name | Type | Default | Description |
|:----------------------|:------|:--------|:----------------------------------------------------------------------|
| `-a`, `--archive` | | | Archive mode (copy all uid/gid information) |
| `--dry-run` | | | Execute command in dry run mode |
| `-L`, `--follow-link` | | | Always follow symbol link in SRC_PATH |
| `--index` | `int` | `0` | Index of the container if there are multiple instances of a service . |
| Name | Type | Default | Description |
|:----------------------|:------|:--------|:--------------------------------------------------------|
| `-a`, `--archive` | | | Archive mode (copy all uid/gid information) |
| `--dry-run` | | | Execute command in dry run mode |
| `-L`, `--follow-link` | | | Always follow symbol link in SRC_PATH |
| `--index` | `int` | `0` | index of the container if service has multiple replicas |
<!---MARKER_GEN_END-->

View File

@@ -5,16 +5,16 @@ Execute a command in a running container.
### Options
| Name | Type | Default | Description |
|:------------------|:--------------|:--------|:----------------------------------------------------------------------------------|
| `-d`, `--detach` | | | Detached mode: Run command in the background. |
| `--dry-run` | | | Execute command in dry run mode |
| `-e`, `--env` | `stringArray` | | Set environment variables |
| `--index` | `int` | `1` | index of the container if there are multiple instances of a service [default: 1]. |
| `-T`, `--no-TTY` | | | Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. |
| `--privileged` | | | Give extended privileges to the process. |
| `-u`, `--user` | `string` | | Run the command as this user. |
| `-w`, `--workdir` | `string` | | Path to workdir directory for this command. |
| Name | Type | Default | Description |
|:------------------|:--------------|:--------|:---------------------------------------------------------------------------------|
| `-d`, `--detach` | | | Detached mode: Run command in the background. |
| `--dry-run` | | | Execute command in dry run mode |
| `-e`, `--env` | `stringArray` | | Set environment variables |
| `--index` | `int` | `0` | index of the container if service has multiple replicas |
| `-T`, `--no-TTY` | | | Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. |
| `--privileged` | | | Give extended privileges to the process. |
| `-u`, `--user` | `string` | | Run the command as this user. |
| `-w`, `--workdir` | `string` | | Path to workdir directory for this command. |
<!---MARKER_GEN_END-->

View File

@@ -8,7 +8,7 @@ Print the public port for a port binding.
| Name | Type | Default | Description |
|:-------------|:---------|:--------|:--------------------------------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
| `--index` | `int` | `1` | index of the container if service has multiple replicas |
| `--index` | `int` | `0` | index of the container if service has multiple replicas |
| `--protocol` | `string` | `tcp` | tcp or udp |

View File

@@ -347,6 +347,7 @@ options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -21,6 +21,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: true
experimental: false
experimentalcli: true
kubernetes: false

View File

@@ -69,6 +69,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: true
kubernetes: false

View File

@@ -29,6 +29,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: true
kubernetes: false

View File

@@ -159,6 +159,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -152,6 +152,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -10,7 +10,7 @@ options:
- option: all
value_type: bool
default_value: "false"
description: Copy to all the containers of the service.
description: copy to all the containers of the service.
deprecated: true
hidden: true
experimental: false
@@ -42,8 +42,7 @@ options:
- option: index
value_type: int
default_value: "0"
description: |
Index of the container if there are multiple instances of a service .
description: index of the container if service has multiple replicas
deprecated: false
hidden: false
experimental: false
@@ -62,6 +61,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -90,6 +90,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -73,6 +73,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -46,6 +46,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -33,9 +33,8 @@ options:
swarm: false
- option: index
value_type: int
default_value: "1"
description: |
index of the container if there are multiple instances of a service [default: 1].
default_value: "0"
description: index of the container if service has multiple replicas
deprecated: false
hidden: false
experimental: false
@@ -118,6 +117,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -38,6 +38,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -43,6 +43,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -91,6 +91,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -58,6 +58,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -17,6 +17,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -7,7 +7,7 @@ plink: docker_compose.yaml
options:
- option: index
value_type: int
default_value: "1"
default_value: "0"
description: index of the container if service has multiple replicas
deprecated: false
hidden: false
@@ -37,6 +37,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -180,6 +180,7 @@ examples: |-
The `docker compose ps` command currently only supports the `--filter status=<status>`
option, but additional filter options may be added in the future.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -124,6 +124,7 @@ examples: |-
`docker compose pull` will try to pull image for services with a build section. If pull fails, it will let
user know this service image MUST be built. You can skip this by setting `--ignore-buildable` flag
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -66,6 +66,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -48,6 +48,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -76,6 +76,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -286,6 +286,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -16,6 +16,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -29,6 +29,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -23,6 +23,7 @@ examples: |-
root 142353 142331 2 15:33 ? 00:00:00 ping localhost -c 5
```
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -16,6 +16,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -285,6 +285,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -37,6 +37,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -27,6 +27,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

26
go.mod
View File

@@ -6,15 +6,15 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Microsoft/go-winio v0.6.1
github.com/buger/goterm v1.0.4
github.com/compose-spec/compose-go v1.15.1
github.com/compose-spec/compose-go v1.17.0
github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.7.2
github.com/cucumber/godog v0.0.0-00010101000000-000000000000 // replaced; see replace for the actual version used
github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493
github.com/docker/buildx v0.11.0
github.com/docker/cli v24.0.2+incompatible
github.com/docker/cli-docs-tool v0.5.1
github.com/docker/docker v24.0.2+incompatible
github.com/docker/buildx v0.11.2
github.com/docker/cli v24.0.4+incompatible
github.com/docker/cli-docs-tool v0.6.0
github.com/docker/docker v24.0.5-0.20230714235725-36e9e796c6fc+incompatible // v24.0.5-dev
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.5.0
github.com/fsnotify/fsevents v0.1.1
@@ -24,12 +24,12 @@ require (
github.com/jonboulle/clockwork v0.4.0
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/buildkit v0.11.0-rc3.0.20230609092854-67a08623b95a
github.com/moby/buildkit v0.12.1-0.20230717122532-faa0cc7da353 // v0.12 release branch
github.com/moby/patternmatcher v0.5.0
github.com/moby/term v0.5.0
github.com/morikuni/aec v1.0.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0-rc3
github.com/opencontainers/image-spec v1.1.0-rc4
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
@@ -44,9 +44,9 @@ require (
go.opentelemetry.io/otel/trace v1.14.0
go.uber.org/goleak v1.2.1
golang.org/x/sync v0.3.0
google.golang.org/grpc v1.56.0
google.golang.org/grpc v1.56.2
gopkg.in/yaml.v2 v2.4.0
gotest.tools/v3 v3.4.0
gotest.tools/v3 v3.5.0
)
require (
@@ -71,7 +71,6 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/containerd/continuity v0.4.1 // indirect
github.com/containerd/ttrpc v1.2.2 // indirect
github.com/containerd/typeurl/v2 v2.1.1 // indirect
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
github.com/cucumber/messages-go/v16 v16.0.1 // indirect
@@ -148,9 +147,9 @@ require (
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tonistiigi/fsutil v0.0.0-20230407161946-9e7a6df48576 // indirect
github.com/tonistiigi/fsutil v0.0.0-20230629203738-36ef4d8c0dbb // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect
github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
@@ -163,7 +162,8 @@ require (
go.opentelemetry.io/otel/metric v0.37.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.7.0 // indirect

54
go.sum
View File

@@ -131,8 +131,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/compose-spec/compose-go v1.15.1 h1:0yaEt6/66dLN0bNWYDTj0CDx626uCdQ9ipJVIJx8O8M=
github.com/compose-spec/compose-go v1.15.1/go.mod h1:3yngGBGfls6FHGQsg4B1z6gz8ej9SOvmAJtxCwgbcnc=
github.com/compose-spec/compose-go v1.17.0 h1:cvje90CU94dQyTnJoHJYjx9yE4Iggse1XmGcO3Qi5ts=
github.com/compose-spec/compose-go v1.17.0/go.mod h1:zR2tP1+kZHi5vJz7PjpW6oMoDji/Js3GHjP+hfjf70Q=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
@@ -145,7 +145,6 @@ github.com/containerd/nydus-snapshotter v0.8.2 h1:7SOrMU2YmLzfbsr5J7liMZJlNi5WT6
github.com/containerd/stargz-snapshotter v0.14.3 h1:OTUVZoPSPs8mGgmQUE1dqw3WX/3nrsmsurW7UPLWl1U=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs=
github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak=
github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4=
github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -167,17 +166,17 @@ github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zA
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493 h1:fm5DpBD+A7o0+x9Nf+o9/4/qPGbfxLpr9qIPVuV8vQc=
github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493/go.mod h1:+fqBJ4vPYo4Uu1ZE4d+bUtTLRXfdSL3NvCZIZ9GHv58=
github.com/docker/buildx v0.11.0 h1:DNCOIYT/7J0sPBlU/ozEhFd4MtbnbFByn45yeTMHXVU=
github.com/docker/buildx v0.11.0/go.mod h1:Yq7ZNjrwXKzW0uSFMk46dl5Gl903k5+bp6U4apsM5rs=
github.com/docker/cli v24.0.2+incompatible h1:QdqR7znue1mtkXIJ+ruQMGQhpw2JzMJLRXp6zpzF6tM=
github.com/docker/cli v24.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.5.1 h1:jIk/cCZurZERhALPVKhqlNxTQGxn2kcI+56gE57PQXg=
github.com/docker/cli-docs-tool v0.5.1/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o=
github.com/docker/buildx v0.11.2 h1:R3p9F0gnI4FwvQ0p40UwdX1T4ugap4UWxY3TFHoP4Ws=
github.com/docker/buildx v0.11.2/go.mod h1:CWAABt10iIuGpleypA3103mplDfcGu0A2AvT03xfpTc=
github.com/docker/cli v24.0.4+incompatible h1:Y3bYF9ekNTm2VFz5U/0BlMdJy73D+Y1iAAZ8l63Ydzw=
github.com/docker/cli v24.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.6.0 h1:Z9x10SaZgFaB6jHgz3OWooynhSa40CsWkpe5hEnG/qA=
github.com/docker/cli-docs-tool v0.6.0/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.2+incompatible h1:eATx+oLz9WdNVkQrr0qjQ8HvRJ4bOOxfzEo8R+dA3cg=
github.com/docker/docker v24.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v24.0.5-0.20230714235725-36e9e796c6fc+incompatible h1:sdGvA1bxu/1J51gAs1XU0bZC+2WxncYnI210as3c6g8=
github.com/docker/docker v24.0.5-0.20230714235725-36e9e796c6fc+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@@ -465,8 +464,8 @@ github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/buildkit v0.11.0-rc3.0.20230609092854-67a08623b95a h1:1k3bAXwxC2N1FncWijq/43sLj2OVIZ11FT0APIXWhMg=
github.com/moby/buildkit v0.11.0-rc3.0.20230609092854-67a08623b95a/go.mod h1:4sM7BBBqXOQ+vV6LrVAOAMhZI9cVNYV5RhZCl906a64=
github.com/moby/buildkit v0.12.1-0.20230717122532-faa0cc7da353 h1:/ZIwqvOF3QKObJbjX96xVvAKtnWdw/AuEqysbbujaZA=
github.com/moby/buildkit v0.12.1-0.20230717122532-faa0cc7da353/go.mod h1:+n9GmkxwBCjVz4u7wmiyh+oqvjIjQM+1zk3iJrWfdos=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
@@ -511,8 +510,8 @@ github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk=
github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
github.com/opencontainers/runtime-spec v1.1.0-rc.2 h1:ucBtEms2tamYYW/SvGpvq9yUN0NEVL6oyLEwDcTSrk8=
@@ -557,7 +556,6 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
@@ -579,7 +577,6 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -626,12 +623,12 @@ github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4D
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g=
github.com/tonistiigi/fsutil v0.0.0-20230407161946-9e7a6df48576 h1:fZXPQDVh5fm2x7pA0CH1TtH80tiZ0L7i834kZqZN8Pw=
github.com/tonistiigi/fsutil v0.0.0-20230407161946-9e7a6df48576/go.mod h1:q1CxMSzcAbjUkVGHoZeQUcCaALnaE4XdWk+zJcgMYFw=
github.com/tonistiigi/fsutil v0.0.0-20230629203738-36ef4d8c0dbb h1:uUe8rNyVXM8moActoBol6Xf6xX2GMr7SosR2EywMvGg=
github.com/tonistiigi/fsutil v0.0.0-20230629203738-36ef4d8c0dbb/go.mod h1:SxX/oNQ/ag6Vaoli547ipFK9J7BZn5JqJG0JE8lf8bA=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f h1:DLpt6B5oaaS8jyXHa9VA4rrZloBVPVXeCtrOsrFauxc=
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 h1:Y/M5lygoNPKwVNLMPXgVfsRT40CSFKXCxuU8LoHySjs=
github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
github.com/weppos/publicsuffix-go v0.15.1-0.20220329081811-9a40b608a236 h1:vMJBP3PQViZsF6cOINtvyMC8ptpLsyJ4EwyFnzuWNxc=
github.com/weppos/publicsuffix-go v0.15.1-0.20220329081811-9a40b608a236/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
@@ -716,6 +713,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -741,8 +740,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -863,7 +862,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -1039,8 +1037,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE=
google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1088,8 +1086,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

107
internal/sync/docker_cp.go Normal file
View File

@@ -0,0 +1,107 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sync
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus"
)
type ComposeClient interface {
Exec(ctx context.Context, projectName string, options api.RunOptions) (int, error)
Copy(ctx context.Context, projectName string, options api.CopyOptions) error
}
type DockerCopy struct {
client ComposeClient
projectName string
infoWriter io.Writer
}
var _ Syncer = &DockerCopy{}
func NewDockerCopy(projectName string, client ComposeClient, infoWriter io.Writer) *DockerCopy {
return &DockerCopy{
projectName: projectName,
client: client,
infoWriter: infoWriter,
}
}
func (d *DockerCopy) Sync(ctx context.Context, service types.ServiceConfig, paths []PathMapping) error {
var errs []error
for i := range paths {
if err := d.sync(ctx, service, paths[i]); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func (d *DockerCopy) sync(ctx context.Context, service types.ServiceConfig, pathMapping PathMapping) error {
scale := 1
if service.Deploy != nil && service.Deploy.Replicas != nil {
scale = int(*service.Deploy.Replicas)
}
if fi, statErr := os.Stat(pathMapping.HostPath); statErr == nil {
if fi.IsDir() {
for i := 1; i <= scale; i++ {
_, err := d.client.Exec(ctx, d.projectName, api.RunOptions{
Service: pathMapping.Service,
Command: []string{"mkdir", "-p", pathMapping.ContainerPath},
Index: i,
})
if err != nil {
logrus.Warnf("failed to create %q from %s: %v", pathMapping.ContainerPath, pathMapping.Service, err)
}
}
fmt.Fprintf(d.infoWriter, "%s created\n", pathMapping.ContainerPath)
} else {
err := d.client.Copy(ctx, d.projectName, api.CopyOptions{
Source: pathMapping.HostPath,
Destination: fmt.Sprintf("%s:%s", pathMapping.Service, pathMapping.ContainerPath),
})
if err != nil {
return err
}
fmt.Fprintf(d.infoWriter, "%s updated\n", pathMapping.ContainerPath)
}
} else if errors.Is(statErr, fs.ErrNotExist) {
for i := 1; i <= scale; i++ {
_, err := d.client.Exec(ctx, d.projectName, api.RunOptions{
Service: pathMapping.Service,
Command: []string{"rm", "-rf", pathMapping.ContainerPath},
Index: i,
})
if err != nil {
logrus.Warnf("failed to delete %q from %s: %v", pathMapping.ContainerPath, pathMapping.Service, err)
}
}
fmt.Fprintf(d.infoWriter, "%s deleted from service\n", pathMapping.ContainerPath)
}
return nil
}

44
internal/sync/shared.go Normal file
View File

@@ -0,0 +1,44 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sync
import (
"context"
"github.com/compose-spec/compose-go/types"
)
// PathMapping contains the Compose service and modified host system path.
type PathMapping struct {
// Service that the file event is for.
Service string
// HostPath that was created/modified/deleted outside the container.
//
// This is the path as seen from the user's perspective, e.g.
// - C:\Users\moby\Documents\hello-world\main.go (file on Windows)
// - /Users/moby/Documents/hello-world (directory on macOS)
HostPath string
// ContainerPath for the target file inside the container (only populated
// for sync events, not rebuild).
//
// This is the path as used in Docker CLI commands, e.g.
// - /workdir/main.go
// - /workdir/subdir
ContainerPath string
}
type Syncer interface {
Sync(ctx context.Context, service types.ServiceConfig, paths []PathMapping) error
}

View File

@@ -0,0 +1,152 @@
/*
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 tracing
import (
"strings"
"time"
"github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// SpanOptions is a small helper type to make it easy to share the options helpers between
// downstream functions that accept slices of trace.SpanStartOption and trace.EventOption.
type SpanOptions []trace.SpanStartEventOption
func (s SpanOptions) SpanStartOptions() []trace.SpanStartOption {
out := make([]trace.SpanStartOption, len(s))
for i := range s {
out[i] = s[i]
}
return out
}
func (s SpanOptions) EventOptions() []trace.EventOption {
out := make([]trace.EventOption, len(s))
for i := range s {
out[i] = s[i]
}
return out
}
// ProjectOptions returns common attributes from a Compose project.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ProjectOptions(proj *types.Project) SpanOptions {
if proj == nil {
return nil
}
disabledServiceNames := make([]string, len(proj.DisabledServices))
for i := range proj.DisabledServices {
disabledServiceNames[i] = proj.DisabledServices[i].Name
}
attrs := []attribute.KeyValue{
attribute.String("project.name", proj.Name),
attribute.String("project.dir", proj.WorkingDir),
attribute.StringSlice("project.compose_files", proj.ComposeFiles),
attribute.StringSlice("project.services.active", proj.ServiceNames()),
attribute.StringSlice("project.services.disabled", disabledServiceNames),
attribute.StringSlice("project.profiles", proj.Profiles),
attribute.StringSlice("project.volumes", proj.VolumeNames()),
attribute.StringSlice("project.networks", proj.NetworkNames()),
attribute.StringSlice("project.secrets", proj.SecretNames()),
attribute.StringSlice("project.configs", proj.ConfigNames()),
attribute.StringSlice("project.extensions", keys(proj.Extensions)),
}
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
// ServiceOptions returns common attributes from a Compose service.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ServiceOptions(service types.ServiceConfig) SpanOptions {
attrs := []attribute.KeyValue{
attribute.String("service.name", service.Name),
attribute.String("service.image", service.Image),
attribute.StringSlice("service.networks", keys(service.Networks)),
}
configNames := make([]string, len(service.Configs))
for i := range service.Configs {
configNames[i] = service.Configs[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.configs", configNames))
secretNames := make([]string, len(service.Secrets))
for i := range service.Secrets {
secretNames[i] = service.Secrets[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.secrets", secretNames))
volNames := make([]string, len(service.Volumes))
for i := range service.Volumes {
volNames[i] = service.Volumes[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.volumes", volNames))
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
// ContainerOptions returns common attributes from a Moby container.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ContainerOptions(container moby.Container) SpanOptions {
attrs := []attribute.KeyValue{
attribute.String("container.id", container.ID),
attribute.String("container.image", container.Image),
unixTimeAttr("container.created_at", container.Created),
}
if len(container.Names) != 0 {
attrs = append(attrs, attribute.String("container.name", strings.TrimPrefix(container.Names[0], "/")))
}
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
func keys[T any](m map[string]T) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func timeAttr(key string, value time.Time) attribute.KeyValue {
return attribute.String(key, value.Format(time.RFC3339))
}
func unixTimeAttr(key string, value int64) attribute.KeyValue {
return timeAttr(key, time.Unix(value, 0).UTC())
}

91
internal/tracing/wrap.go Normal file
View File

@@ -0,0 +1,91 @@
/*
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 tracing
import (
"context"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
"go.opentelemetry.io/otel/trace"
)
// SpanWrapFunc wraps a function that takes a context with a trace.Span, marking the status as codes.Error if the
// wrapped function returns an error.
//
// The context passed to the function is created from the span to ensure correct propagation.
//
// NOTE: This function is nearly identical to SpanWrapFuncForErrGroup, except the latter is designed specially for
// convenience with errgroup.Group due to its prevalence throughout the codebase. The code is duplicated to avoid
// adding even more levels of function wrapping/indirection.
func SpanWrapFunc(spanName string, opts SpanOptions, fn func(ctx context.Context) error) func(context.Context) error {
return func(ctx context.Context) error {
ctx, span := Tracer.Start(ctx, spanName, opts.SpanStartOptions()...)
defer span.End()
if err := fn(ctx); err != nil {
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
}
}
// SpanWrapFuncForErrGroup wraps a function that takes a context with a trace.Span, marking the status as codes.Error
// if the wrapped function returns an error.
//
// The context passed to the function is created from the span to ensure correct propagation.
//
// NOTE: This function is nearly identical to SpanWrapFunc, except this function is designed specially for
// convenience with errgroup.Group due to its prevalence throughout the codebase. The code is duplicated to avoid
// adding even more levels of function wrapping/indirection.
func SpanWrapFuncForErrGroup(ctx context.Context, spanName string, opts SpanOptions, fn func(ctx context.Context) error) func() error {
return func() error {
ctx, span := Tracer.Start(ctx, spanName, opts.SpanStartOptions()...)
defer span.End()
if err := fn(ctx); err != nil {
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
}
}
// EventWrapFuncForErrGroup invokes a function and records an event, optionally including the returned
// error as the "exception message" on the event.
//
// This is intended for lightweight usage to wrap errgroup.Group calls where a full span is not desired.
func EventWrapFuncForErrGroup(ctx context.Context, eventName string, opts SpanOptions, fn func(ctx context.Context) error) func() error {
return func() error {
span := trace.SpanFromContext(ctx)
eventOpts := opts.EventOptions()
err := fn(ctx)
if err != nil {
eventOpts = append(eventOpts, trace.WithAttributes(semconv.ExceptionMessage(err.Error())))
}
span.AddEvent(eventName, eventOpts...)
return err
}
}

View File

@@ -145,7 +145,6 @@ func (o BuildOptions) Apply(project *types.Project) error {
if service.Build == nil {
continue
}
service.Image = GetImageNameOrDefault(service, project.Name)
if platform != "" {
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, platform) {
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", service.Name, platform)

View File

@@ -70,7 +70,7 @@ type execDetails struct {
}
// NewDryRunClient produces a DryRunClient
func NewDryRunClient(apiClient client.APIClient, cli *command.DockerCli) (*DryRunClient, error) {
func NewDryRunClient(apiClient client.APIClient, cli command.Cli) (*DryRunClient, error) {
b, err := builder.New(cli, builder.WithSkippedValidation())
if err != nil {
return nil, err

View File

@@ -217,7 +217,7 @@ func (s *ServiceProxy) List(ctx context.Context, options ListOptions) ([]Stack,
return s.ListFn(ctx, options)
}
// Convert implements Service interface
// Config implements Service interface
func (s *ServiceProxy) Config(ctx context.Context, project *types.Project, options ConfigOptions) ([]byte, error) {
if s.ConfigFn == nil {
return nil, ErrNotImplemented

View File

@@ -22,6 +22,8 @@ import (
"os"
"path/filepath"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/buildx/controller/pb"
"github.com/compose-spec/compose-go/types"
@@ -77,7 +79,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
return nil, err
}
builtIDs := make([]string, len(project.Services))
builtDigests := make([]string, len(project.Services))
err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
return nil
@@ -94,11 +96,11 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
} else {
service.Build.Args = service.Build.Args.OverrideBy(args)
}
id, err := s.doBuildClassic(ctx, service, options)
id, err := s.doBuildClassic(ctx, project.Name, service, options)
if err != nil {
return err
}
builtIDs[idx] = id
builtDigests[idx] = id
if options.Push {
return s.push(ctx, project, api.PushOptions{})
@@ -120,7 +122,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
if err != nil {
return err
}
builtIDs[idx] = digest
builtDigests[idx] = digest
return nil
}, func(traversal *graphTraversal) {
@@ -137,9 +139,10 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
}
imageIDs := map[string]string{}
for i, d := range builtIDs {
if d != "" {
imageIDs[project.Services[i].Image] = d
for i, imageDigest := range builtDigests {
if imageDigest != "" {
imageRef := api.GetImageNameOrDefault(project.Services[i], project.Name)
imageIDs[imageRef] = imageDigest
}
}
return imageIDs, err
@@ -169,7 +172,11 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
return err
}
err = s.pullRequiredImages(ctx, project, images, quietPull)
err = tracing.SpanWrapFunc("project/pull", tracing.ProjectOptions(project),
func(ctx context.Context) error {
return s.pullRequiredImages(ctx, project, images, quietPull)
},
)(ctx)
if err != nil {
return err
}
@@ -185,16 +192,24 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
}
if buildRequired {
builtImages, err := s.build(ctx, project, api.BuildOptions{
Progress: mode,
})
err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(project),
func(ctx context.Context) error {
builtImages, err := s.build(ctx, project, api.BuildOptions{
Progress: mode,
})
if err != nil {
return err
}
for name, digest := range builtImages {
images[name] = digest
}
return nil
},
)(ctx)
if err != nil {
return err
}
for name, digest := range builtImages {
images[name] = digest
}
}
// set digest as com.docker.compose.image label so we can detect outdated containers
@@ -222,7 +237,8 @@ func (s *composeService) prepareProjectForBuild(project *types.Project, images m
continue
}
_, localImagePresent := images[service.Image]
image := api.GetImageNameOrDefault(service, project.Name)
_, localImagePresent := images[image]
if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
service.Build = nil
project.Services[i] = service
@@ -292,8 +308,6 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
}
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
tags := []string{service.Image}
buildArgs := flatten(service.Build.Args.Resolve(envResolver(project.Environment)))
for k, v := range storeutil.GetProxyConfig(s.dockerCli) {
@@ -335,6 +349,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
sessionConfig = append(sessionConfig, secretsProvider)
}
tags := []string{api.GetImageNameOrDefault(service, project.Name)}
if len(service.Build.Tags) > 0 {
tags = append(tags, service.Build.Tags...)
}
@@ -345,18 +360,19 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
imageLabels := getImageBuildLabels(project, service)
push := options.Push && service.Image != ""
exports := []bclient.ExportEntry{{
Type: "docker",
Attrs: map[string]string{
"load": "true",
"push": fmt.Sprint(options.Push),
"push": fmt.Sprint(push),
},
}}
if len(service.Build.Platforms) > 1 {
exports = []bclient.ExportEntry{{
Type: "image",
Attrs: map[string]string{
"push": fmt.Sprint(options.Push),
"push": fmt.Sprint(push),
},
}}
}

View File

@@ -45,7 +45,7 @@ import (
)
//nolint:gocyclo
func (s *composeService) doBuildClassic(ctx context.Context, service types.ServiceConfig, options api.BuildOptions) (string, error) {
func (s *composeService) doBuildClassic(ctx context.Context, projectName string, service types.ServiceConfig, options api.BuildOptions) (string, error) {
var (
buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser
@@ -160,7 +160,8 @@ func (s *composeService) doBuildClassic(ctx context.Context, service types.Servi
authConfigs[k] = registry.AuthConfig(auth)
}
buildOptions := imageBuildOptions(service.Build)
buildOptions.Tags = append(buildOptions.Tags, service.Image)
imageName := api.GetImageNameOrDefault(service, projectName)
buildOptions.Tags = append(buildOptions.Tags, imageName)
buildOptions.Dockerfile = relDockerfile
buildOptions.AuthConfigs = authConfigs
buildOptions.Memory = options.Memory

View File

@@ -88,8 +88,11 @@ func (s *composeService) DryRunMode(ctx context.Context, dryRun bool) (context.C
if err != nil {
return ctx, err
}
err = cli.Initialize(flags.NewClientOptions(), command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
return api.NewDryRunClient(s.apiClient(), cli)
options := flags.NewClientOptions()
options.Context = s.dockerCli.CurrentContext()
err = cli.Initialize(options, command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
return api.NewDryRunClient(s.apiClient(), s.dockerCli)
}))
if err != nil {
return ctx, err
@@ -200,6 +203,7 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
condition := ServiceConditionRunningOrHealthy
// Let's restart the dependency by default if we don't have the info stored in the label
restart := true
required := true
dependency := dcArr[0]
// backward compatibility
@@ -209,7 +213,7 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
restart, _ = strconv.ParseBool(dcArr[2])
}
}
service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart}
service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
}
}
project.Services = append(project.Services, *service)

View File

@@ -20,6 +20,7 @@ import (
"context"
"fmt"
"sort"
"strconv"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
@@ -72,7 +73,9 @@ func getDefaultFilters(projectName string, oneOff oneOff, selectedServices ...st
func (s *composeService) getSpecifiedContainer(ctx context.Context, projectName string, oneOff oneOff, stopped bool, serviceName string, containerIndex int) (moby.Container, error) {
defaultFilters := getDefaultFilters(projectName, oneOff, serviceName)
defaultFilters = append(defaultFilters, containerNumberFilter(containerIndex))
if containerIndex > 0 {
defaultFilters = append(defaultFilters, containerNumberFilter(containerIndex))
}
containers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs(
defaultFilters...,
@@ -83,8 +86,16 @@ func (s *composeService) getSpecifiedContainer(ctx context.Context, projectName
return moby.Container{}, err
}
if len(containers) < 1 {
return moby.Container{}, fmt.Errorf("service %q is not running container #%d", serviceName, containerIndex)
if containerIndex > 0 {
return moby.Container{}, fmt.Errorf("service %q is not running container #%d", serviceName, containerIndex)
}
return moby.Container{}, fmt.Errorf("service %q is not running", serviceName)
}
sort.Slice(containers, func(i, j int) bool {
x, _ := strconv.Atoi(containers[i].Labels[api.ContainerNumberLabel])
y, _ := strconv.Atoi(containers[j].Labels[api.ContainerNumberLabel])
return x < y
})
container := containers[0]
return container, nil
}

View File

@@ -25,8 +25,12 @@ import (
"sync"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/compose-spec/compose-go/types"
"github.com/containerd/containerd/platforms"
"github.com/docker/compose/v2/internal/tracing"
moby "github.com/docker/docker/api/types"
containerType "github.com/docker/docker/api/types/container"
specs "github.com/opencontainers/image-spec/specs-go/v1"
@@ -93,17 +97,19 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options
return err
}
strategy := options.RecreateDependencies
if utils.StringContains(options.Services, name) {
strategy = options.Recreate
}
err = c.ensureService(ctx, project, service, strategy, options.Inherit, options.Timeout)
if err != nil {
return err
}
return tracing.SpanWrapFunc("service/apply", tracing.ServiceOptions(service), func(ctx context.Context) error {
strategy := options.RecreateDependencies
if utils.StringContains(options.Services, name) {
strategy = options.Recreate
}
err = c.ensureService(ctx, project, service, strategy, options.Inherit, options.Timeout)
if err != nil {
return err
}
c.updateProject(project, name)
return nil
c.updateProject(project, name)
return nil
})(ctx)
})
}
@@ -179,7 +185,8 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
if i >= expected {
// Scale Down
container := container
eg.Go(func() error {
traceOpts := append(tracing.ServiceOptions(service), tracing.ContainerOptions(container)...)
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "service/scale/down", traceOpts, func(ctx context.Context) error {
timeoutInSecond := utils.DurationSecondToInt(timeout)
err := c.service.apiClient().ContainerStop(ctx, container.ID, containerType.StopOptions{
Timeout: timeoutInSecond,
@@ -188,7 +195,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
return err
}
return c.service.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
})
}))
continue
}
@@ -198,11 +205,11 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
}
if mustRecreate {
i, container := i, container
eg.Go(func() error {
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "container/recreate", tracing.ContainerOptions(container), func(ctx context.Context) error {
recreated, err := c.service.recreateContainer(ctx, project, service, container, inherit, timeout)
updated[i] = recreated
return err
})
}))
continue
}
@@ -218,9 +225,9 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
w.Event(progress.CreatedEvent(name))
default:
container := container
eg.Go(func() error {
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/start", tracing.ContainerOptions(container), func(ctx context.Context) error {
return c.service.startContainer(ctx, container)
})
}))
}
updated[i] = container
}
@@ -231,7 +238,8 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
number := next + i
name := getContainerName(project.Name, service, number)
i := i
eg.Go(func() error {
eventOpts := tracing.SpanOptions{trace.WithAttributes(attribute.String("container.name", name))}
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/scale/up", eventOpts, func(ctx context.Context) error {
opts := createOptions{
AutoRemove: false,
AttachStdin: false,
@@ -241,7 +249,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
container, err := c.service.createContainer(ctx, project, service, name, number, opts)
updated[actual+i] = container
return err
})
}))
continue
}
@@ -286,9 +294,18 @@ func containerEvents(containers Containers, eventFunc func(string) progress.Even
return events
}
func containerSkippedEvents(containers Containers, eventFunc func(string, string) progress.Event, reason string) []progress.Event {
events := []progress.Event{}
for _, container := range containers {
events = append(events, eventFunc(getContainerProgressName(container), reason))
}
return events
}
// ServiceConditionRunningOrHealthy is a service condition on status running or healthy
const ServiceConditionRunningOrHealthy = "running_or_healthy"
//nolint:gocyclo
func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, dependencies types.DependsOnConfig, containers Containers) error {
eg, _ := errgroup.WithContext(ctx)
w := progress.ContextWriter(ctx)
@@ -312,6 +329,11 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
case ServiceConditionRunningOrHealthy:
healthy, err := s.isServiceHealthy(ctx, waitingFor, true)
if err != nil {
if !config.Required {
w.Events(containerSkippedEvents(waitingFor, progress.SkippedEvent, fmt.Sprintf("optional dependency %q is not running or is unhealthy", dep)))
logrus.Warnf("optional dependency %q is not running or is unhealthy: %s", dep, err.Error())
return nil
}
return err
}
if healthy {
@@ -321,6 +343,11 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
case types.ServiceConditionHealthy:
healthy, err := s.isServiceHealthy(ctx, waitingFor, false)
if err != nil {
if !config.Required {
w.Events(containerSkippedEvents(waitingFor, progress.SkippedEvent, fmt.Sprintf("optional dependency %q failed to start", dep)))
logrus.Warnf("optional dependency %q failed to start: %s", dep, err.Error())
return nil
}
w.Events(containerEvents(waitingFor, progress.ErrorEvent))
return errors.Wrap(err, "dependency failed to start")
}
@@ -334,6 +361,12 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
return err
}
if exited {
logMessageSuffix := fmt.Sprintf("%q didn't complete successfully: exit %d", dep, code)
if !config.Required {
w.Events(containerSkippedEvents(waitingFor, progress.SkippedEvent, fmt.Sprintf("optional dependency %s", logMessageSuffix)))
logrus.Warnf("optional dependency %s", logMessageSuffix)
return nil
}
w.Events(containerEvents(waitingFor, progress.Exited))
if code != 0 {
return fmt.Errorf("service %q didn't complete successfully: exit %d", dep, code)
@@ -549,13 +582,15 @@ func (s *composeService) createMobyContainer(ctx context.Context,
// call via container.NetworkMode & network.NetworkingConfig
// any remaining networks are connected one-by-one here after creation (but before start)
serviceNetworks := service.NetworksByPriority()
if len(serviceNetworks) > 1 {
for _, networkKey := range serviceNetworks[1:] {
mobyNetworkName := project.Networks[networkKey].Name
epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases)
if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil {
return created, err
}
for _, networkKey := range serviceNetworks {
mobyNetworkName := project.Networks[networkKey].Name
if string(cfgs.Host.NetworkMode) == mobyNetworkName {
// primary network already configured as part of ContainerCreate
continue
}
epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases)
if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil {
return created, err
}
}

View File

@@ -236,8 +236,8 @@ func TestWaitDependencies(t *testing.T) {
redisService := types.ServiceConfig{Name: "redis", Scale: 1}
project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{dbService, redisService}}
dependencies := types.DependsOnConfig{
"db": {Condition: types.ServiceConditionStarted},
"redis": {Condition: types.ServiceConditionStarted},
"db": {Condition: types.ServiceConditionStarted, Required: true},
"redis": {Condition: types.ServiceConditionStarted, Required: true},
}
assert.NilError(t, tested.waitDependencies(context.Background(), &project, dependencies, nil))
})

View File

@@ -139,6 +139,7 @@ func prepareVolumes(p *types.Project) error {
p.Services[i].DependsOn[service.Name].Condition == "" {
p.Services[i].DependsOn[service.Name] = types.ServiceDependency{
Condition: types.ServiceConditionStarted,
Required: true,
}
}
}
@@ -862,7 +863,7 @@ func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount
target := config.Target
if config.Target == "" {
target = configsBaseDir + config.Source
} else if !isUnixAbs(config.Target) {
} else if !isAbsTarget(config.Target) {
target = configsBaseDir + config.Target
}
@@ -897,7 +898,7 @@ func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount
target := secret.Target
if secret.Target == "" {
target = secretsDir + secret.Source
} else if !isUnixAbs(secret.Target) {
} else if !isAbsTarget(secret.Target) {
target = secretsDir + secret.Target
}
@@ -928,10 +929,24 @@ func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount
return values, nil
}
func isAbsTarget(p string) bool {
return isUnixAbs(p) || isWindowsAbs(p)
}
func isUnixAbs(p string) bool {
return strings.HasPrefix(p, "/")
}
func isWindowsAbs(p string) bool {
if strings.HasPrefix(p, "\\\\") {
return true
}
if len(p) > 2 && p[1] == ':' {
return p[2] == '\\'
}
return false
}
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.

View File

@@ -124,7 +124,7 @@ func TestPrepareVolumes(t *testing.T) {
Name: "aService",
VolumesFrom: []string{"anotherService"},
DependsOn: map[string]composetypes.ServiceDependency{
"anotherService": {Condition: composetypes.ServiceConditionHealthy},
"anotherService": {Condition: composetypes.ServiceConditionHealthy, Required: true},
},
},
{

View File

@@ -268,10 +268,15 @@ func NewGraph(project *types.Project, initialStatus ServiceStatus) (*Graph, erro
graph.AddVertex(s.Name, s.Name, initialStatus)
}
for _, s := range project.Services {
for index, s := range project.Services {
for _, name := range s.GetDependencies() {
err := graph.AddEdge(s.Name, name)
if err != nil {
if !s.DependsOn[name].Required {
delete(s.DependsOn, name)
project.Services[index] = s
continue
}
if api.IsNotFoundError(err) {
ds, err := project.GetDisabledService(name)
if err == nil {

View File

@@ -24,25 +24,14 @@ import (
"github.com/docker/compose/v2/pkg/api"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
)
func (s *composeService) Port(ctx context.Context, projectName string, service string, port uint16, options api.PortOptions) (string, int, error) {
projectName = strings.ToLower(projectName)
list, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs(
projectFilter(projectName),
serviceFilter(service),
containerNumberFilter(options.Index),
),
})
container, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, service, options.Index)
if err != nil {
return "", 0, err
}
if len(list) == 0 {
return "", 0, fmt.Errorf("no container found for %s%s%d", service, api.Separator, options.Index)
}
container := list[0]
for _, p := range container.Ports {
if p.PrivatePort == port && p.Type == options.Protocol {
return p.IP, int(p.PublicPort), nil

View File

@@ -313,8 +313,15 @@ func isServiceImageToBuild(service types.ServiceConfig, services []types.Service
return true
}
for _, depService := range services {
if depService.Image == service.Image && depService.Build != nil {
if service.Image == "" {
// N.B. this should be impossible as service must have either `build` or `image` (or both)
return false
}
// look through the other services to see if another has a build definition for the same
// image name
for _, svc := range services {
if svc.Image == service.Image && svc.Build != nil {
return true
}
}

View File

@@ -48,7 +48,7 @@ func (s *composeService) restart(ctx context.Context, projectName string, option
}
}
// ignore depends_on relations which are not impacted by restarting service
// ignore depends_on relations which are not impacted by restarting service or not required
for i, service := range project.Services {
for name, r := range service.DependsOn {
if !r.Restart {

View File

@@ -18,6 +18,7 @@ package compose
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
@@ -36,11 +37,6 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
return 0, err
}
start := cmd.NewStartOptions()
start.OpenStdin = !opts.Detach && opts.Interactive
start.Attach = !opts.Detach
start.Containers = []string{containerID}
// remove cancellable context signal handler so we can forward signals to container without compose to exit
signal.Reset()
@@ -49,9 +45,14 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
go cmd.ForwardAllSignals(ctx, s.dockerCli, containerID, sigc)
defer signal.Stop(sigc)
err = cmd.RunStart(s.dockerCli, &start)
if sterr, ok := err.(cli.StatusError); ok {
return sterr.StatusCode, nil
err = cmd.RunStart(s.dockerCli, &cmd.StartOptions{
OpenStdin: !opts.Detach && opts.Interactive,
Attach: !opts.Detach,
Containers: []string{containerID},
})
var stErr cli.StatusError
if errors.As(err, &stErr) {
return stErr.StatusCode, nil
}
return 0, err
}

View File

@@ -58,7 +58,7 @@ func createTar(env string, config types.ServiceSecretConfig) (bytes.Buffer, erro
value := []byte(env)
b := bytes.Buffer{}
tarWriter := tar.NewWriter(&b)
mode := uint32(0o400)
mode := uint32(0o444)
if config.Mode != nil {
mode = *config.Mode
}
@@ -66,7 +66,7 @@ func createTar(env string, config types.ServiceSecretConfig) (bytes.Buffer, erro
target := config.Target
if config.Target == "" {
target = "/run/secrets/" + config.Source
} else if !isUnixAbs(config.Target) {
} else if !isAbsTarget(config.Target) {
target = "/run/secrets/" + config.Target
}

View File

@@ -108,6 +108,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
for _, s := range project.Services {
depends[s.Name] = types.ServiceDependency{
Condition: getDependencyCondition(s, project),
Required: true,
}
}
if options.WaitTimeout > 0 {

View File

@@ -23,6 +23,8 @@ import (
"os/signal"
"syscall"
"github.com/docker/compose/v2/internal/tracing"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli"
"github.com/docker/compose/v2/pkg/api"
@@ -31,7 +33,7 @@ import (
)
func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error {
err := progress.Run(ctx, func(ctx context.Context) error {
err := progress.Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(project), func(ctx context.Context) error {
err := s.create(ctx, project, options.Create)
if err != nil {
return err
@@ -40,7 +42,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
return s.start(ctx, project.Name, options.Start, nil)
}
return nil
}, s.stdinfo())
}), s.stdinfo())
if err != nil {
return err
}

View File

@@ -52,7 +52,7 @@ func (s *composeService) Viz(_ context.Context, project *types.Project, opts api
// dot is the perfect layout for this use case since graph is directed and hierarchical
graphBuilder.WriteString(opts.Indentation + "layout=dot;\n")
addNodes(&graphBuilder, graph, &opts)
addNodes(&graphBuilder, graph, project.Name, &opts)
graphBuilder.WriteByte('\n')
addEdges(&graphBuilder, graph, &opts)
@@ -63,7 +63,7 @@ func (s *composeService) Viz(_ context.Context, project *types.Project, opts api
// addNodes adds the corresponding graphviz representation of all the nodes in the given graph to the graphBuilder
// returns the same graphBuilder
func addNodes(graphBuilder *strings.Builder, graph vizGraph, opts *api.VizOptions) *strings.Builder {
func addNodes(graphBuilder *strings.Builder, graph vizGraph, projectName string, opts *api.VizOptions) *strings.Builder {
for serviceNode := range graph {
// write:
// "service name" [style="filled" label<<font point-size="15">service name</font>
@@ -107,7 +107,7 @@ func addNodes(graphBuilder *strings.Builder, graph vizGraph, opts *api.VizOption
if opts.IncludeImageName {
graphBuilder.WriteString("<font point-size=\"10\">")
graphBuilder.WriteString("<br/><br/><b>Image:</b><br/>")
graphBuilder.WriteString(serviceNode.Image)
graphBuilder.WriteString(api.GetImageNameOrDefault(*serviceNode, projectName))
graphBuilder.WriteString("</font>")
}

View File

@@ -1,6 +1,6 @@
/*
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
@@ -17,13 +17,13 @@ package compose
import (
"context"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/docker/compose/v2/internal/sync"
"github.com/compose-spec/compose-go/types"
"github.com/jonboulle/clockwork"
"github.com/mitchellh/mapstructure"
@@ -54,11 +54,8 @@ type Trigger struct {
const quietPeriod = 2 * time.Second
// fileMapping contains the Compose service and modified host system path.
//
// For file sync, the container path is also included.
// For rebuild, there is no container path, so it is always empty.
type fileMapping struct {
// fileEvent contains the Compose service and modified host system path.
type fileEvent struct {
// Service that the file event is for.
Service string
// HostPath that was created/modified/deleted outside the container.
@@ -67,17 +64,11 @@ type fileMapping struct {
// - C:\Users\moby\Documents\hello-world\main.go
// - /Users/moby/Documents/hello-world/main.go
HostPath string
// ContainerPath for the target file inside the container (only populated
// for sync events, not rebuild).
//
// This is the path as used in Docker CLI commands, e.g.
// - /workdir/main.go
ContainerPath string
}
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, _ api.WatchOptions) error { //nolint: gocyclo
needRebuild := make(chan fileMapping)
needSync := make(chan fileMapping)
needRebuild := make(chan fileEvent)
needSync := make(chan sync.PathMapping)
_, err := s.prepareProjectForBuild(project, nil)
if err != nil {
@@ -175,7 +166,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
return eg.Wait()
}
func (s *composeService) watch(ctx context.Context, name string, watcher watch.Notify, triggers []Trigger, needSync chan fileMapping, needRebuild chan fileMapping) error {
func (s *composeService) watch(ctx context.Context, name string, watcher watch.Notify, triggers []Trigger, needSync chan sync.PathMapping, needRebuild chan fileEvent) error {
ignores := make([]watch.PathMatcher, len(triggers))
for i, trigger := range triggers {
ignore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
@@ -209,11 +200,6 @@ WATCH:
fmt.Fprintf(s.stdinfo(), "change detected on %s\n", hostPath)
f := fileMapping{
HostPath: hostPath,
Service: name,
}
switch trigger.Action {
case WatchActionSync:
logrus.Debugf("modified file %s triggered sync", hostPath)
@@ -221,12 +207,18 @@ WATCH:
if err != nil {
return err
}
// always use Unix-style paths for inside the container
f.ContainerPath = path.Join(trigger.Target, rel)
needSync <- f
needSync <- sync.PathMapping{
Service: name,
HostPath: hostPath,
// always use Unix-style paths for inside the container
ContainerPath: path.Join(trigger.Target, rel),
}
case WatchActionRebuild:
logrus.Debugf("modified file %s requires image to be rebuilt", hostPath)
needRebuild <- f
needRebuild <- fileEvent{
HostPath: hostPath,
Service: name,
}
default:
return fmt.Errorf("watch action %q is not supported", trigger)
}
@@ -304,57 +296,25 @@ func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Proje
}
}
func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project, needSync <-chan fileMapping) func() error {
func (s *composeService) makeSyncFn(
ctx context.Context,
project *types.Project,
needSync <-chan sync.PathMapping,
) func() error {
syncer := sync.NewDockerCopy(project.Name, s, s.stdinfo())
return func() error {
for {
select {
case <-ctx.Done():
return nil
case opt := <-needSync:
service, err := project.GetService(opt.Service)
case pathMapping := <-needSync:
service, err := project.GetService(pathMapping.Service)
if err != nil {
return err
}
scale := 1
if service.Deploy != nil && service.Deploy.Replicas != nil {
scale = int(*service.Deploy.Replicas)
}
if fi, statErr := os.Stat(opt.HostPath); statErr == nil {
if fi.IsDir() {
for i := 1; i <= scale; i++ {
_, err := s.Exec(ctx, project.Name, api.RunOptions{
Service: opt.Service,
Command: []string{"mkdir", "-p", opt.ContainerPath},
Index: i,
})
if err != nil {
logrus.Warnf("failed to create %q from %s: %v", opt.ContainerPath, opt.Service, err)
}
}
fmt.Fprintf(s.stdinfo(), "%s created\n", opt.ContainerPath)
} else {
err := s.Copy(ctx, project.Name, api.CopyOptions{
Source: opt.HostPath,
Destination: fmt.Sprintf("%s:%s", opt.Service, opt.ContainerPath),
})
if err != nil {
return err
}
fmt.Fprintf(s.stdinfo(), "%s updated\n", opt.ContainerPath)
}
} else if errors.Is(statErr, fs.ErrNotExist) {
for i := 1; i <= scale; i++ {
_, err := s.Exec(ctx, project.Name, api.RunOptions{
Service: opt.Service,
Command: []string{"rm", "-rf", opt.ContainerPath},
Index: i,
})
if err != nil {
logrus.Warnf("failed to delete %q from %s: %v", opt.ContainerPath, opt.Service, err)
}
}
fmt.Fprintf(s.stdinfo(), "%s deleted from service\n", opt.ContainerPath)
if err := syncer.Sync(ctx, service, []sync.PathMapping{pathMapping}); err != nil {
return err
}
}
}
@@ -363,7 +323,7 @@ func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project,
type rebuildServices map[string]utils.Set[string]
func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, input <-chan fileMapping, fn func(services rebuildServices)) {
func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, input <-chan fileEvent, fn func(services rebuildServices)) {
services := make(rebuildServices)
t := clock.NewTimer(delay)
defer t.Stop()

View File

@@ -19,6 +19,8 @@ import (
"testing"
"time"
"github.com/docker/compose/v2/internal/sync"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/watch"
"github.com/jonboulle/clockwork"
@@ -27,7 +29,7 @@ import (
)
func Test_debounce(t *testing.T) {
ch := make(chan fileMapping)
ch := make(chan fileEvent)
var (
ran int
got []string
@@ -47,7 +49,7 @@ func Test_debounce(t *testing.T) {
return nil
})
for i := 0; i < 100; i++ {
ch <- fileMapping{Service: "test"}
ch <- fileEvent{Service: "test"}
}
assert.Equal(t, ran, 0)
clock.Advance(quietPeriod)
@@ -79,8 +81,8 @@ func (t testWatcher) Errors() chan error {
}
func Test_sync(t *testing.T) {
needSync := make(chan fileMapping)
needRebuild := make(chan fileMapping)
needSync := make(chan sync.PathMapping)
needRebuild := make(chan fileEvent)
ctx, cancelFunc := context.WithCancel(context.TODO())
defer cancelFunc()
@@ -119,7 +121,7 @@ func Test_sync(t *testing.T) {
watcher.Events() <- watch.NewFileEvent("/src/changed")
select {
case actual := <-needSync:
assert.DeepEqual(t, fileMapping{Service: "test", HostPath: "/src/changed", ContainerPath: "/work/changed"}, actual)
assert.DeepEqual(t, sync.PathMapping{Service: "test", HostPath: "/src/changed", ContainerPath: "/work/changed"}, actual)
case <-time.After(100 * time.Millisecond):
t.Error("timeout")
}

View File

@@ -111,7 +111,7 @@ func TestLocalComposeBuild(t *testing.T) {
t.Run(env+" no rebuild when up again", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"), res.Stdout())
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"))
})
t.Run(env+" rebuild when up --build", func(t *testing.T) {
@@ -121,6 +121,11 @@ func TestLocalComposeBuild(t *testing.T) {
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
})
t.Run(env+" build --push ignored for unnamed images", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--workdir", "fixtures/build-test", "build", "--push", "nginx")
assert.Assert(t, !strings.Contains(res.Stdout(), "failed to push"), res.Stdout())
})
t.Run(env+" cleanup build project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
c.RunDockerCmd(t, "rmi", "build-test-nginx")

View File

@@ -35,6 +35,12 @@ func TestLocalComposeExec(t *testing.T) {
return ret
}
cleanup := func() {
c.RunDockerComposeCmd(t, cmdArgs("down", "--timeout=0")...)
}
cleanup()
t.Cleanup(cleanup)
c.RunDockerComposeCmd(t, cmdArgs("up", "-d")...)
t.Run("exec true", func(t *testing.T) {

View File

@@ -136,4 +136,20 @@ func TestLocalComposeRun(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "down", "--remove-orphans")
})
t.Run("run without dependencies", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "run", "--no-deps", "service_a")
assert.Assert(t, !strings.Contains(res.Combined(), "shared_dep"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "service_b"), res.Combined())
c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/deps.yaml", "down", "--remove-orphans")
})
t.Run("run with not required dependency", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "run", "foo")
assert.Assert(t, strings.Contains(res.Combined(), "foo"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "bar"), res.Combined())
c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "down", "--remove-orphans")
})
}

View File

@@ -281,8 +281,12 @@ func TestStopWithDependenciesAttached(t *testing.T) {
const projectName = "compose-e2e-stop-with-deps"
c := NewParallelCLI(t, WithEnv("COMMAND=echo hello"))
t.Run("up", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "-p", projectName, "up", "--attach-dependencies", "foo")
res.Assert(t, icmd.Expected{Out: "exited with code 0"})
})
cleanup := func() {
c.RunDockerComposeCmd(t, "-p", projectName, "down", "--remove-orphans", "--timeout=0")
}
cleanup()
t.Cleanup(cleanup)
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "-p", projectName, "up", "--attach-dependencies", "foo")
res.Assert(t, icmd.Expected{Out: "exited with code 0"})
}

View File

@@ -51,8 +51,18 @@ func TestUpExitCodeFrom(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-exit-code-from"
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--exit-code-from=failure", "failure")
res.Assert(t, icmd.Expected{ExitCode: 42})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}
func TestUpExitCodeFromContainerKilled(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-exit-code-from-kill"
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--exit-code-from=test")
res.Assert(t, icmd.Expected{ExitCode: 137})
res.Assert(t, icmd.Expected{ExitCode: 143})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}

View File

@@ -1,10 +1,12 @@
services:
base:
image: base
init: true
build:
context: .
dockerfile: base.dockerfile
service:
init: true
depends_on:
- base
build:

View File

@@ -4,6 +4,7 @@ services:
command: echo 'hello world'
longrunning:
image: alpine
init: true
depends_on:
oneshot:
condition: service_completed_successfully

View File

@@ -0,0 +1,11 @@
services:
foo:
image: bash
command: echo "foo"
depends_on:
bar:
required: false
condition: service_healthy
bar:
image: nginx:alpine
profiles: [not-required]

View File

@@ -3,6 +3,7 @@ services:
my-service:
image: alpine
command: tail -f /dev/null
init: true
depends_on:
nginx: {condition: service_healthy}

View File

@@ -1,6 +1,7 @@
services:
foo:
image: alpine
init: true
entrypoint: ["sleep", "600"]
networks:
default:
@@ -9,4 +10,4 @@ networks:
default:
ipam:
config:
- subnet: 10.1.0.0/16
- subnet: 10.1.0.0/16

View File

@@ -1,6 +1,7 @@
services:
ping:
image: alpine
init: true
command: ping localhost -c ${REPEAT:-1}
hello:
image: alpine

View File

@@ -6,12 +6,14 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
db:
image: gtardif/sentences-db
init: true
networks:
- dbnet
- closesnetworkname1
- closesnetworkname2
words:
image: gtardif/sentences-api
init: true
ports:
- "8080:8080"
networks:
@@ -19,6 +21,7 @@ services:
- servicenet
web:
image: gtardif/sentences-web
init: true
ports:
- "80:80"
labels:

View File

@@ -1,12 +1,14 @@
services:
with-restart:
image: alpine
init: true
command: tail -f /dev/null
depends_on:
nginx: {condition: service_healthy, restart: true}
no-restart:
image: alpine
init: true
command: tail -f /dev/null
depends_on:
nginx: { condition: service_healthy }

View File

@@ -1,4 +1,5 @@
services:
restart:
image: alpine
init: true
command: ash -c "if [[ -f /tmp/restart.lock ]] ; then sleep infinity; else touch /tmp/restart.lock; fi"

View File

@@ -1,12 +1,15 @@
services:
db:
image: gtardif/sentences-db
init: true
words:
image: gtardif/sentences-api
init: true
ports:
- "95:8080"
web:
image: gtardif/sentences-web
init: true
ports:
- "90:80"
labels:

View File

@@ -1,6 +1,7 @@
services:
fail:
image: alpine
init: true
command: sleep infinity
healthcheck:
test: "false"
@@ -8,6 +9,7 @@ services:
retries: 3
depends:
image: alpine
init: true
command: sleep infinity
depends_on:
fail:

View File

@@ -1,11 +1,14 @@
services:
safe:
image: 'alpine'
init: true
command: ['/bin/sh', '-c', 'sleep infinity'] # never exiting
failure:
image: 'alpine'
command: ['/bin/sh', '-c', 'sleep 2 ; echo "exiting" ; exit 42']
init: true
command: ['/bin/sh', '-c', 'sleep 1 ; echo "exiting with error" ; exit 42']
test:
image: 'alpine'
init: true
command: ['/bin/sh', '-c', 'sleep 99999 ; echo "tests are OK"'] # very long job
depends_on: [safe]

View File

@@ -1,6 +1,7 @@
services:
stderr:
image: alpine
init: true
command: /bin/ash /log_to_stderr.sh
volumes:
- ./log_to_stderr.sh:/log_to_stderr.sh

View File

@@ -0,0 +1,34 @@
x-dev: &x-dev
watch:
- action: sync
path: ./data
target: /app/data
ignore:
- '*.foo'
- ./ignored
services:
alpine:
build:
dockerfile_inline: |-
FROM alpine
RUN mkdir -p /app/data
init: true
command: sleep infinity
x-develop: *x-dev
busybox:
build:
dockerfile_inline: |-
FROM busybox
RUN mkdir -p /app/data
init: true
command: sleep infinity
x-develop: *x-dev
debian:
build:
dockerfile_inline: |-
FROM debian
RUN mkdir -p /app/data
init: true
command: sleep infinity
x-develop: *x-dev

View File

@@ -0,0 +1 @@
hello world

View File

@@ -29,47 +29,34 @@ import (
func TestNetworks(t *testing.T) {
// fixture is shared with TestNetworkModes and is not safe to run concurrently
c := NewCLI(t)
const projectName = "network-e2e"
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME="+projectName,
"COMPOSE_FILE=./fixtures/network-test/compose.yaml",
))
t.Run("ensure we do not reuse previous networks", func(t *testing.T) {
c.RunDockerOrExitError(t, "network", "rm", projectName+"-dbnet")
c.RunDockerOrExitError(t, "network", "rm", "microservices")
})
c.RunDockerComposeCmd(t, "down", "-t0", "-v")
t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/network-test/compose.yaml", "--project-name", projectName, "up",
"-d")
})
c.RunDockerComposeCmd(t, "up", "-d")
t.Run("check running project", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-p", projectName, "ps")
res.Assert(t, icmd.Expected{Out: `web`})
res := c.RunDockerComposeCmd(t, "ps")
res.Assert(t, icmd.Expected{Out: `web`})
endpoint := "http://localhost:80"
output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, `"word":`))
endpoint := "http://localhost:80"
output := HTTPGetWithRetry(t, endpoint+"/words/noun", http.StatusOK, 2*time.Second, 20*time.Second)
assert.Assert(t, strings.Contains(output, `"word":`))
res = c.RunDockerCmd(t, "network", "ls")
res.Assert(t, icmd.Expected{Out: projectName + "_dbnet"})
res.Assert(t, icmd.Expected{Out: "microservices"})
})
res = c.RunDockerCmd(t, "network", "ls")
res.Assert(t, icmd.Expected{Out: projectName + "_dbnet"})
res.Assert(t, icmd.Expected{Out: "microservices"})
t.Run("port", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "port", "words", "8080")
res.Assert(t, icmd.Expected{Out: `0.0.0.0:8080`})
})
res = c.RunDockerComposeCmd(t, "port", "words", "8080")
res.Assert(t, icmd.Expected{Out: `0.0.0.0:8080`})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
t.Run("check networks after down", func(t *testing.T) {
res := c.RunDockerCmd(t, "network", "ls")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "microservices"), res.Combined())
})
c.RunDockerComposeCmd(t, "down", "-t0", "-v")
res = c.RunDockerCmd(t, "network", "ls")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "microservices"), res.Combined())
}
func TestNetworkAliases(t *testing.T) {

View File

@@ -153,3 +153,16 @@ func TestScaleDoesntRecreate(t *testing.T) {
assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
}
func TestUpWithDependencyNotRequired(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-dependency-not-required"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/deps-not-required.yaml", "--project-name", projectName,
"--profile", "not-required", "up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "foo"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), " optional dependency \"bar\" failed to start"), res.Combined())
}

View File

@@ -21,6 +21,8 @@ import (
"testing"
"time"
"gotest.tools/v3/icmd"
"gotest.tools/v3/assert"
)
@@ -28,6 +30,12 @@ func TestWaitOnFaster(t *testing.T) {
const projectName = "e2e-wait-faster"
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/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "faster")
}
@@ -36,6 +44,12 @@ func TestWaitOnSlower(t *testing.T) {
const projectName = "e2e-wait-slower"
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/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "slower")
}
@@ -44,12 +58,27 @@ func TestWaitOnInfinity(t *testing.T) {
const projectName = "e2e-wait-infinity"
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/wait/compose.yaml", "--project-name", projectName, "up", "-d")
cmd := c.NewDockerComposeCmd(t, "--project-name", projectName, "wait", "infinity")
r := icmd.StartCmd(cmd)
assert.NilError(t, r.Error)
t.Cleanup(func() {
if r.Cmd.Process != nil {
_ = r.Cmd.Process.Kill()
}
})
finished := make(chan struct{})
ticker := time.NewTicker(7 * time.Second)
go func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "infinity")
_ = r.Cmd.Wait()
finished <- struct{}{}
}()
@@ -64,6 +93,12 @@ func TestWaitAndDrop(t *testing.T) {
const projectName = "e2e-wait-and-drop"
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/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "--down-project", "faster")

189
pkg/e2e/watch_test.go Normal file
View File

@@ -0,0 +1,189 @@
/*
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 (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"github.com/distribution/distribution/v3/uuid"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)
func TestWatch(t *testing.T) {
if runtime.GOOS == "darwin" {
t.Skip("Test currently broken on macOS due to symlink issues (see compose-go#436)")
}
services := []string{"alpine", "busybox", "debian"}
for _, svcName := range services {
t.Run(svcName, func(t *testing.T) {
t.Helper()
doTest(t, svcName)
})
}
}
// NOTE: these tests all share a single Compose file but are safe to run concurrently
func doTest(t *testing.T, svcName string) {
tmpdir := t.TempDir()
dataDir := filepath.Join(tmpdir, "data")
writeDataFile := func(name string, contents string) {
t.Helper()
dest := filepath.Join(dataDir, name)
require.NoError(t, os.MkdirAll(filepath.Dir(dest), 0o700))
t.Logf("writing %q to %q", contents, dest)
require.NoError(t, os.WriteFile(dest, []byte(contents+"\n"), 0o600))
}
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
CopyFile(t, filepath.Join("fixtures", "watch", "compose.yaml"), composeFilePath)
projName := "e2e-watch-" + svcName
env := []string{
"COMPOSE_FILE=" + composeFilePath,
"COMPOSE_PROJECT_NAME=" + projName,
}
cli := NewCLI(t, WithEnv(env...))
cleanup := func() {
cli.RunDockerComposeCmd(t, "down", svcName, "--timeout=0", "--remove-orphans", "--volumes")
}
cleanup()
t.Cleanup(cleanup)
cli.RunDockerComposeCmd(t, "up", svcName, "--wait", "--build")
cmd := cli.NewDockerComposeCmd(t, "--verbose", "alpha", "watch", svcName)
// stream output since watch runs in the background
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
r := icmd.StartCmd(cmd)
require.NoError(t, r.Error)
t.Cleanup(func() {
// IMPORTANT: watch doesn't exit on its own, don't leak processes!
if r.Cmd.Process != nil {
_ = r.Cmd.Process.Kill()
}
})
var testComplete atomic.Bool
go func() {
// if the process exits abnormally before the test is done, fail the test
if err := r.Cmd.Wait(); err != nil && !testComplete.Load() {
assert.Check(t, cmp.Nil(err))
}
}()
require.NoError(t, os.Mkdir(dataDir, 0o700))
checkFileContents := func(path string, contents string) poll.Check {
return func(pollLog poll.LogT) poll.Result {
if r.Cmd.ProcessState != nil {
return poll.Error(fmt.Errorf("watch process exited early: %s", r.Cmd.ProcessState))
}
res := icmd.RunCmd(cli.NewDockerComposeCmd(t, "exec", svcName, "cat", path))
if strings.Contains(res.Stdout(), contents) {
return poll.Success()
}
return poll.Continue(res.Combined())
}
}
waitForFlush := func() {
sentinelVal := uuid.Generate().String()
writeDataFile("wait.txt", sentinelVal)
poll.WaitOn(t, checkFileContents("/app/data/wait.txt", sentinelVal))
}
t.Logf("Writing to a file until Compose watch is up and running")
poll.WaitOn(t, func(t poll.LogT) poll.Result {
writeDataFile("hello.txt", "hello world")
return checkFileContents("/app/data/hello.txt", "hello world")(t)
})
t.Logf("Modifying file contents")
writeDataFile("hello.txt", "hello watch")
poll.WaitOn(t, checkFileContents("/app/data/hello.txt", "hello watch"))
t.Logf("Deleting file")
require.NoError(t, os.Remove(filepath.Join(dataDir, "hello.txt")))
waitForFlush()
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/hello.txt").
Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such file or directory",
},
)
t.Logf("Writing to ignored paths")
writeDataFile("data.foo", "ignored")
writeDataFile(filepath.Join("ignored", "hello.txt"), "ignored")
waitForFlush()
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/data.foo").
Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such file or directory",
},
)
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/ignored").
Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such file or directory",
},
)
t.Logf("Creating subdirectory")
require.NoError(t, os.Mkdir(filepath.Join(dataDir, "subdir"), 0o700))
waitForFlush()
cli.RunDockerComposeCmd(t, "exec", svcName, "stat", "/app/data/subdir")
t.Logf("Writing to file in subdirectory")
writeDataFile(filepath.Join("subdir", "file.txt"), "a")
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "a"))
t.Logf("Writing to file multiple times")
writeDataFile(filepath.Join("subdir", "file.txt"), "x")
writeDataFile(filepath.Join("subdir", "file.txt"), "y")
writeDataFile(filepath.Join("subdir", "file.txt"), "z")
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "z"))
writeDataFile(filepath.Join("subdir", "file.txt"), "z")
writeDataFile(filepath.Join("subdir", "file.txt"), "y")
writeDataFile(filepath.Join("subdir", "file.txt"), "x")
poll.WaitOn(t, checkFileContents("/app/data/subdir/file.txt", "x"))
t.Logf("Deleting directory")
require.NoError(t, os.RemoveAll(filepath.Join(dataDir, "subdir")))
waitForFlush()
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/subdir").
Assert(t, icmd.Expected{
ExitCode: 1,
Err: "No such file or directory",
},
)
testComplete.Store(true)
}

View File

@@ -47,3 +47,9 @@ func (s Set[T]) Elements() []T {
}
return elements
}
func (s Set[T]) RemoveAll(elements ...T) {
for _, e := range elements {
s.Remove(e)
}
}

View File

@@ -29,7 +29,7 @@ func Contains[T any](origin []T, element T) bool {
return false
}
// RemoveAll removes all elements from origin slice
// Remove removes all elements from origin slice
func Remove[T any](origin []T, elements ...T) []T {
var filtered []T
for _, v := range origin {