Compare commits

..

106 Commits

Author SHA1 Message Date
Nicolas De Loof
f06caeb844 oras doesn't prepend index.docker.io to repository ref
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-07 09:26:13 +02:00
Nicolas De Loof
49d1bc7524 introduce push --repository
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-07-05 09:06:41 +02:00
Guillaume Lours
827e864ed0 Merge pull request #10745 from glours/add-builder-support
add support of --builder and BUILDX_BUILDER
2023-07-03 10:46:02 +02:00
Guillaume Lours
28301fb1a4 add support of --builder and BUILDX_BUILDER
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-07-03 10:11:18 +02:00
Ulysses Souza
fa3e16c66b Merge pull request #10742 from ulyssessouza/add-wait
Add `docker compose wait`
2023-07-02 13:54:34 +02:00
Ulysses Souza
edd76bfd70 Add docker compose wait
Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com>
2023-06-30 16:07:03 +02:00
Milas Bowman
c496c23071 ci: upgrade compose-go to v1.15.1 (#10757)
* Fix for "`build.context` is required" errors

https://github.com/compose-spec/compose-go/releases/tag/v1.15.1

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-29 16:50:10 -04:00
Guillaume Lours
02284378bf Merge pull request #10756 from milas/network-race
up: fix race condition on network connect
2023-06-29 22:47:29 +02:00
Milas Bowman
10b290e682 up: fix race condition on network connect
Engine API only allows at most one network to be connected as
part of the ContainerCreate API request. Compose will pick the
highest priority network.

Afterwards, the remaining networks (if any) are connected before
the container is actually started.

The big change here is that, previously, the highest-priority
network was connected in the create, and then disconnected and
immediately reconnected along with all the others. This was
racy because evidently connecting the container to the network
as part of the create isn't synchronous, so sometimes when Compose
tried to disconnect it, the API would return an error like:
```
container <id> is not connected to the network <network>
```

To avoid needing to disconnect and immediately reconnect, the
network config logic has been refactored to ensure that it sets
up the network config correctly the first time.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-29 16:00:55 -04:00
aroramrinaal
3906a7a67c Updated documentation files for my contribution
Ran 'make docs' to generate updated doc files from the reference.

Signed-off-by: aroramrinaal <aroramrinaal@gmail.com>
2023-06-27 22:48:17 +02:00
aroramrinaal
83671db3dd Fix capitalization error in sentence by adding an uppercase letter at beginning
Signed-off-by: aroramrinaal <aroramrinaal@gmail.com>
2023-06-27 22:48:17 +02:00
Jes Cok
1a41678c58 fix typos
Signed-off-by: Jes Cok <xigua67damn@gmail.com>
2023-06-27 16:12:25 +02:00
Guillaume Lours
035276e027 watch: add warning when a path is already used by a bind mount volume (#10741)
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-26 18:56:04 -04:00
Guillaume Lours
db24023884 Merge pull request #10720 from docker/dependabot/go_modules/google.golang.org/grpc-1.56.0
build(deps): bump google.golang.org/grpc from 1.53.0 to 1.56.0
2023-06-21 13:02:20 +02:00
dependabot[bot]
c48f542962 build(deps): bump google.golang.org/grpc from 1.53.0 to 1.56.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.53.0 to 1.56.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.53.0...v1.56.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-21 12:08:40 +02:00
Guillaume Lours
42dc7a6a87 Merge pull request #10730 from glours/bump-compose-go-1.15.0
bump compose-go to version v1.15.0
2023-06-21 11:58:37 +02:00
Guillaume Lours
30b3b47383 bump compose-go to version v1.15.0
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-21 11:45:36 +02:00
Milas Bowman
061b52da9a ci: build fix for new buildx
Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-21 11:17:39 +02:00
Milas Bowman
04aa155878 ci: upgrade to buildx v0.11
https://github.com/docker/buildx/releases/tag/v0.11.0

Several `replace` directives have been removed and dependencies
aligned with buildx as needed.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-21 11:17:39 +02:00
Guillaume Lours
2d4f8d31fc Merge pull request #10709 from ndeloof/secret_uid
warn user build.secrets uid,gid,mode are not implemented
2023-06-21 10:19:43 +02:00
Milas Bowman
e1f8603a62 otel: refactor root command span reporting
* Move all the initialization code out of `main.go`
* Ensure spans are reported when there's an error with the
  command
* Attach the Compose version & active Docker context to the
  resource instead of the span
* Name the root CLI span `cli/<cmd>` for clarity and grab
  the full subcommand path (e.g. `alpha-viz` instead of just
  `viz`)

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-20 17:25:58 +02:00
Nicolas De Loof
a2ce602f6c fix race condition, waiting for containers when one exit
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-20 16:17:15 +02:00
dependabot[bot]
401334e03f build(deps): bump github.com/AlecAivazis/survey/v2 from 2.3.6 to 2.3.7 (#10699)
Bumps [github.com/AlecAivazis/survey/v2](https://github.com/AlecAivazis/survey) from 2.3.6 to 2.3.7.
- [Release notes](https://github.com/AlecAivazis/survey/releases)
- [Commits](https://github.com/AlecAivazis/survey/compare/v2.3.6...v2.3.7)

---
updated-dependencies:
- dependency-name: github.com/AlecAivazis/survey/v2
  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-06-20 09:15:33 -04:00
Guillaume Lours
93cf2b921a use main branch of Docker Desktop repo to trigger remote workflow
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-20 12:14:08 +02:00
Guillaume Lours
83b2433a27 Merge pull request #10724 from glours/add-missing-comma-merge
add missing comma in desktop-edge-test job
2023-06-19 21:02:00 +02:00
Guillaume Lours
c8d06137b5 add missing comma in desktop-edge-test job
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-19 20:01:52 +02:00
Nicolas De Loof
c61b8aa5ac introduce run --cap-add to run maintenance commands using service image
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-19 14:20:20 +02:00
Milas Bowman
ff3984e609 otel: fix initialization / error-handling (#10717)
* If there's no `otel` key (or the value is `null`) in the config,
  don't return an error
* Propagate error from the exporter instead of panicking

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-15 12:43:15 -04:00
Guillaume Lours
2efea2e9f5 Merge pull request #10715 from docker/dependabot/go_modules/golang.org/x/sync-0.3.0
build(deps): bump golang.org/x/sync from 0.2.0 to 0.3.0
2023-06-15 18:03:23 +02:00
dependabot[bot]
6a3a95c4a8 build(deps): bump golang.org/x/sync from 0.2.0 to 0.3.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.2.0 to 0.3.0.
- [Commits](https://github.com/golang/sync/compare/v0.2.0...v0.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-15 17:52:44 +02:00
Guillaume Lours
fee8a1c6c6 Merge pull request #10703 from glours/add-repo-input-dd-token
specify origin repo in generation token step of docker desktop edge testing
2023-06-15 17:47:48 +02:00
Nicolas De Loof
7ffe83dc95 don't apply "rebuild" watch strategy by default
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-15 15:30:21 +02:00
Nicolas De Loof
d20c2551f2 warn user build.secrets uid,gid,mode are not implemented
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-14 10:21:20 +02:00
Guillaume Lours
0e9a5b6b78 specify origin repo in generation token step of docker desktop edge testing
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-13 16:23:51 +02:00
Guillaume Lours
6887a3fc3e Merge pull request #10702 from glours/fix-DD-APP-ID
add vars. prefix to DOCKERDESKTOP_APP_ID
2023-06-13 14:46:52 +02:00
Guillaume Lours
586fe87f98 add vars. prefix to DOCKERDESKTOP_APP_ID
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-13 14:45:26 +02:00
Guillaume Lours
6d66130266 Merge pull request #10701 from glours/fix-DD-APP-ID
use directly DOCKERDESKTOP_APP_ID without env. prefix
2023-06-13 14:21:01 +02:00
Guillaume Lours
0f83a8630e use directly DOCKERDESKTOP_APP_ID without env. prefix
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-13 14:19:56 +02:00
Guillaume Lours
26cb941f79 Merge pull request #10698 from glours/fix-DD-APP-ID
fix typo in merge workflow for DOCKERDESKTOP_APP_ID
2023-06-13 10:23:28 +02:00
Guillaume Lours
43783d36e2 fix typo in merge workflow for DOCKERDESKTOP_APP_ID
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-13 10:18:58 +02:00
Guillaume Lours
08e6bfc859 Merge pull request #10691 from glours/fix-dd-workflow-json-parsing
escape containerimage.digest attribute in merge.yml GHA worlflow
2023-06-13 09:52:13 +02:00
Guillaume Lours
7a870e2449 Update .github/workflows/merge.yml
Co-authored-by: CrazyMax <github@crazymax.dev>
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-12 16:54:21 +02:00
Guillaume Lours
8cd8f08d77 escape containerimage.digest attribut in merge.yml GHA worlflow
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-12 16:54:21 +02:00
Nicolas De Loof
cfe91becc7 use --progress to configure progress UI stylet push
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-12 16:53:42 +02:00
Guillaume Lours
9384e5f4d7 Merge pull request #10694 from milas/dependabot-ignore-more
ci: add more ignore rules to dependabot
2023-06-12 16:32:17 +02:00
robbert-ef
68bd0eb523 cli: fix timeout behavior on up / restart / stop (#10672)
Do not set a hardcoded default timeout of 10 seconds when restarting / stopping but use the container `StopTimeout` (defaults to 10 seconds).

Also fixed custom timeout not used when invoking `up`.

Signed-off-by: Robbert Segeren <robbert.segeren@easyflex.nl>
2023-06-12 09:18:25 -04:00
Milas Bowman
3fe665b93d ci: add more ignore rules to dependabot
Reduce PR spam for dependencies that we manually manage.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-12 09:06:21 -04:00
Guillaume Lours
508d71c5df ci: fix merge workflow (#10685)
use env instead of variables prefix to use `DOCKERDESKTOP_APPID`

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-09 15:35:47 -04:00
Guillaume Lours
58c5ea8217 Merge pull request #10625 from glours/compose-edge-desktop-integration
add GitHub action to trigger Docker Desktop e2e tests with Compose edge version
2023-06-09 21:18:37 +02:00
Guillaume Lours
ec31d3c2ac add GitHub action to trigger Docker Desktop e2e tests with Compose edge version
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-06-09 21:07:03 +02:00
Laura Brehm
599723f890 Merge pull request #10677 from docker/dependabot/go_modules/github.com/Microsoft/go-winio-0.6.1
build(deps): bump github.com/Microsoft/go-winio from 0.5.2 to 0.6.1
2023-06-09 12:13:09 +02:00
dependabot[bot]
0a9b9fd8fe build(deps): bump github.com/Microsoft/go-winio from 0.5.2 to 0.6.1
Bumps [github.com/Microsoft/go-winio](https://github.com/Microsoft/go-winio) from 0.5.2 to 0.6.1.
- [Release notes](https://github.com/Microsoft/go-winio/releases)
- [Commits](https://github.com/Microsoft/go-winio/compare/v0.5.2...v0.6.1)

---
updated-dependencies:
- dependency-name: github.com/Microsoft/go-winio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-09 10:00:25 +00:00
Milas Bowman
3c8a56dbf3 trace: add OTEL initialization (#10526)
This is a bunch of OTEL initialization code. It's all in
`internal/` because there are re-usable parts here, but Compose
isn't the right spot. Once we've stabilized the interfaces a bit
and the need arises, we can move it to a separate module.

Currently, a single span is produced to wrap the root Compose
command.

Compose will respect the standard OTEL environment variables
as well as OTEL metadata from the Docker context. Both can be
used simultaneously. The latter is intended for local system
integration and is restricted to Unix sockets / named pipes.

None of this is enabled by default. It requires setting the
`COMPOSE_EXPERIMENTAL_OTEL=1` environment variable to
gate it during development.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-08 16:46:07 -04:00
Milas Bowman
e63ab14b1e ci: merge Go coverage reports before upload (#10666)
Attempting to fix the state of codecov action checks right now,
which are behaving very erratically.

Using the new functionality in Go 1.20 to merge multiple reports,
so now the unit & E2E coverage data reports are stored as artifacts
and then downloaded, merged, and finally uploaded to codecov as a
new job.

Additionally, add a `codecov.yml` config and try to turn down the
aggressiveness of it for CI checks.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-08 14:58:21 -04:00
Guillaume Lours
32cf776ecd Merge pull request #10620 from ndeloof/Building
do not render `Building` when no build is needed
2023-06-08 12:16:46 +02:00
Guillaume Lours
955784c406 Merge pull request #10662 from milas/bump-deps
ci: upgrade to Go 1.20.5 and Moby v24.x
2023-06-08 12:03:23 +02:00
Guillaume Lours
2d22c2b5ce Merge pull request #10652 from ndeloof/watch_mkdir
create directory in container using `mkdir -p`
2023-06-08 11:39:21 +02:00
Nicolas De Loof
852c9e80b4 create directory in container using mkdir -p
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-08 11:28:22 +02:00
Milas Bowman
37850f7955 ci: upgrade to Go 1.20.5 and Moby v24.x
Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-07 10:41:49 -04:00
Nicolas De Loof
4bf2fe9fed assume we receive logs by lines and don't ignore those without EOL
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-07 10:40:37 +02:00
Guillaume Lours
e21a8d6293 Merge pull request #10657 from ndeloof/compose_project_name
don't skip `compose` used as project name
2023-06-07 08:26:21 +02:00
dependabot[bot]
f8b6459403 build(deps): bump github.com/sirupsen/logrus from 1.9.2 to 1.9.3 (#10653)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.2 to 1.9.3.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.9.2...v1.9.3)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  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-06-06 16:44:25 -04:00
Milas Bowman
be6c9565e3 ci: bump golangci-lint to v1.53.x (#10659)
Requires some changes for depguard config

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-06-06 16:31:41 -04:00
Nicolas De Loof
60fe97416c don't skip compose used as project name
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-06 10:24:59 +02:00
Nicolas De Loof
629c9f62e9 better diagnostic message on network label mismatch
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-05 16:54:58 +02:00
Guillaume Lours
7c3fe359b7 Merge pull request #10622 from ndeloof/logs_follow
fix `compose -p x logs -f` detect new services started after command
2023-06-02 09:39:20 +02:00
Nicolas De Loof
d2aa15c06e bump buildx and use confutil.ConfigDir
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-06-01 12:09:13 +02:00
Guillaume Lours
6530880361 Merge pull request #10623 from jfly/jfly/tweak-warning-message
Fix typo in warning about existing volume
2023-06-01 08:50:33 +02:00
Nicolas De loof
1bd8a773a7 detect network conflict as name is not guaranteed to be unique (#10612)
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-31 14:46:23 -04:00
Nicolas De Loof
fed8ef6b79 forward signal to container
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-31 15:10:11 +02:00
Guillaume Lours
419fcdd6c8 Merge pull request #10604 from aevesdocker/ENGDOCS-1373
update docs to reflect dry run mode is feature complete
2023-05-31 12:49:14 +02:00
Allie Sadler
65b714c108 fix build issue
Signed-off-by: Allie Sadler <102604716+aevesdocker@users.noreply.github.com>
2023-05-31 10:24:49 +01:00
Allie Sadler
44dd232e97 Merge branch 'v2' into ENGDOCS-1373 2023-05-31 10:18:45 +01:00
Guillaume Lours
83ad5e97b7 add Windows drive prefix to temp dir usage in the doc generation task
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-05-31 10:18:23 +02:00
Guillaume Lours
b0a35ccc98 Merge pull request #10632 from docker/dependabot/go_modules/github.com/stretchr/testify-1.8.4
build(deps): bump github.com/stretchr/testify from 1.8.3 to 1.8.4
2023-05-30 12:15:51 +02:00
dependabot[bot]
f5480ee3ed build(deps): bump github.com/stretchr/testify from 1.8.3 to 1.8.4
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.3 to 1.8.4.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.3...v1.8.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-30 10:00:07 +00:00
Nick Sieger
b4924dee83 Merge pull request #10627 from nicksieger/test-cucumber-port-conflict 2023-05-27 08:34:50 -05:00
Nick Sieger
2ca8ab914a e2e: make test re-runnable on the same machine
Signed-off-by: Nick Sieger <nick@nicksieger.com>
2023-05-26 15:54:39 -05:00
Nick Sieger
3ec8c60657 e2e: add a cuke feature to test compose errors with port conflicts
Signed-off-by: Nick Sieger <nick@nicksieger.com>
2023-05-26 15:54:35 -05:00
Nicolas De Loof
06ec06472f up should not silently ignore missing depends_on service
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-26 21:59:29 +02:00
Nicolas De Loof
466e1d3197 prevent buildkt's progress to render Building when no built is needed
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-26 15:04:37 +02:00
Nicolas De Loof
0d6b99e6f9 e2e test to cover logs -f managing service being added/scaled
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-26 14:51:23 +02:00
Nicolas De Loof
01d91c490c detect new container from project have started when running compose logs with no explicit services
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-26 14:15:48 +02:00
Nicolas De Loof
6f6e1635fd compute service hash with a default DeployConfig
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-26 14:15:33 +02:00
Jeremy Fleischman
3d05a1becf Fix typo in warning about existing volume
Previously, this was telling us "but was not created for project
[project-it-was-created-for]", which is wrong. I opted to make the
message super explicit and print both the actual project and the
expected project.

Signed-off-by: Jeremy Fleischman <jeremyfleischman@gmail.com>
2023-05-25 17:16:47 -07:00
Laura Brehm
42cd961d58 Merge pull request #10582 from docker/dependabot/go_modules/github.com/stretchr/testify-1.8.3 2023-05-25 14:16:17 +02:00
dependabot[bot]
d15fcc6444 build(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.3
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.2 to 1.8.3.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.2...v1.8.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-25 12:03:43 +00:00
Laura Brehm
22c2471a08 Merge pull request #10572 from docker/dependabot/go_modules/github.com/cloudflare/cfssl-1.6.4 2023-05-25 13:53:07 +02:00
dependabot[bot]
29a1cc452d build(deps): bump github.com/cloudflare/cfssl from 1.4.1 to 1.6.4
Bumps [github.com/cloudflare/cfssl](https://github.com/cloudflare/cfssl) from 1.4.1 to 1.6.4.
- [Release notes](https://github.com/cloudflare/cfssl/releases)
- [Changelog](https://github.com/cloudflare/cfssl/blob/master/CHANGELOG)
- [Commits](https://github.com/cloudflare/cfssl/compare/v1.4.1...v1.6.4)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/cfssl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-24 21:10:49 +00:00
Milas Bowman
b05a94fd66 progress: remove errant import (#10614)
Write the warning using `logrus.Warn`. The function being used was
coming from `cfssl`'s log package, which was presumably the result
of auto-import being _slightly_ too aggressive.

(Note: `cfssl` is still an indirect dependency after this.)

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-05-24 17:08:46 -04:00
Guillaume Lours
15cad92b61 fix display of volumes flag in down help command
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-05-24 22:11:58 +02:00
Nicolas De Loof
c7afc6188b detect conflict removing volume/image and warn user
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-24 17:32:32 +02:00
Nicolas De Loof
ca19b7fcc9 introduce WithRootNodesAndDown to walk the graph from specified nodes and down
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-24 17:32:32 +02:00
Nicolas De Loof
93bd27a0cc introduce ability to select service to be stopped by compose down
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-24 17:32:32 +02:00
Nicolas De loof
68c462e607 scale: sort containers by creation date to remove older ones first (#10571)
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-23 13:58:32 -04:00
Nicolas De loof
916aac6c27 watch: only monitor configured paths (#10599)
For performance, don't watch the entire build context.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-23 13:35:50 -04:00
Nicolas De loof
eafcd1b35e secrets: only set CopyUIDGID when required (#10598)
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-23 13:34:24 -04:00
Allie Sadler
1e399c271a update docs to reflect dry run mode is feature complete
Signed-off-by: Allie Sadler <102604716+aevesdocker@users.noreply.github.com>
2023-05-23 15:53:48 +01:00
Guillaume Lours
544b579cb0 Merge pull request #10597 from ndeloof/ComposeProjectName
fix support for project name set by COMPOSE_PROJECT_NAME env var
2023-05-22 20:24:13 +02:00
Nicolas De Loof
daa6bec80a fix support for project name set by COMPOSE_PROJECT_NAME env var
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-22 19:14:57 +02:00
Sebastiaan van Stijn
34bd41cc0c go.mod: golang.org/x/oauth2 v0.1.0
docker/docker already depends on oauth2 v0.1.0, which differs only in
dependencies;

https://github.com/golang/oauth2/compare/6fdb5e3db783...v0.1.0

The replace rule is still needed to help with some dependency hell when
resolving the version to use;

    go mod tidy
    go: downloading cloud.google.com/go v0.75.0
    github.com/docker/compose/v2/pkg/compose imports
        github.com/moby/buildkit/client imports
        go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc tested by
        go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.test imports
        google.golang.org/grpc/interop imports
        golang.org/x/oauth2/google imports
        cloud.google.com/go/compute/metadata: ambiguous import: found package cloud.google.com/go/compute/metadata in multiple modules:
        cloud.google.com/go v0.75.0 (/Users/thajeztah/go/pkg/mod/cloud.google.com/go@v0.75.0/compute/metadata)
        cloud.google.com/go/compute/metadata v0.2.1 (/Users/thajeztah/go/pkg/mod/cloud.google.com/go/compute/metadata@v0.2.1)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-05-22 15:01:38 +02:00
Guillaume Lours
70953b18c0 Merge pull request #10591 from ndeloof/fix_detect_swarm_enabled
fix detection of swarm mode
2023-05-22 09:25:56 +02:00
Nicolas De Loof
cfe1a860ff fix detection of swarm mode
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-05-22 08:26:22 +02:00
Laura Brehm
4dcda432cf Merge pull request #10578 from docker/dependabot/go_modules/github.com/sirupsen/logrus-1.9.2
build(deps): bump github.com/sirupsen/logrus from 1.9.0 to 1.9.2
2023-05-18 11:14:10 +01:00
dependabot[bot]
5c2a885647 build(deps): bump github.com/sirupsen/logrus from 1.9.0 to 1.9.2
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.0 to 1.9.2.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.9.0...v1.9.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-18 09:59:38 +00:00
Guillaume Lours
cd0fc214a5 only check the platform of cached image if image found
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-05-17 08:04:54 +02:00
100 changed files with 3029 additions and 1310 deletions

View File

@@ -5,14 +5,20 @@ updates:
schedule:
interval: daily
ignore:
# docker/buildx + docker/cli + docker/docker require coordination to
# ensure compatibility between them
# docker + moby deps require coordination
- dependency-name: "github.com/docker/buildx"
# buildx is still 0.x
update-types: ["version-update:semver-minor"]
- dependency-name: "github.com/moby/buildkit"
# buildkit is still 0.x
update-types: [ "version-update:semver-minor" ]
- dependency-name: "github.com/docker/cli"
# docker/cli uses CalVer rather than SemVer
update-types: ["version-update:semver-major", "version-update:semver-minor"]
update-types: ["version-update:semver-major"]
- dependency-name: "github.com/docker/docker"
# docker/docker uses CalVer rather than SemVer
update-types: ["version-update:semver-major", "version-update:semver-minor"]
update-types: ["version-update:semver-major"]
- dependency-name: "github.com/containerd/containerd"
# containerd major/minor must be kept in sync with moby
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
- dependency-name: "go.opentelemetry.io/otel/*"
# OTEL is v1.x but has some parts that are not API stable yet
update-types: [ "version-update:semver-major", "version-update:semver-minor"]

View File

@@ -19,7 +19,6 @@ on:
default: "false"
env:
DESTDIR: "./bin"
DOCKER_CLI_VERSION: "20.10.17"
permissions:
@@ -103,7 +102,7 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: compose
path: ${{ env.DESTDIR }}/*
path: ./bin/release/*
if-no-files-found: error
test:
@@ -124,13 +123,15 @@ jobs:
*.cache-from=type=gha,scope=test
*.cache-to=type=gha,scope=test
-
name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
name: Gather coverage data
uses: actions/upload-artifact@v3
with:
name: coverage-data-unit
path: bin/coverage/unit/
if-no-files-found: error
e2e:
runs-on: ubuntu-latest
env:
DESTDIR: "./bin/build"
strategy:
fail-fast: false
matrix:
@@ -179,11 +180,17 @@ jobs:
name: Test plugin mode
if: ${{ matrix.mode == 'plugin' }}
run: |
rm -rf ./covdatafiles
mkdir ./covdatafiles
make e2e-compose GOCOVERDIR=covdatafiles
go tool covdata textfmt -i=covdatafiles -o=coverage.out
rm -rf ./bin/coverage/e2e
mkdir -p ./bin/coverage/e2e
make e2e-compose GOCOVERDIR=bin/coverage/e2e TEST_FLAGS="-v"
-
name: Gather coverage data
if: ${{ matrix.mode == 'plugin' }}
uses: actions/upload-artifact@v3
with:
name: coverage-data-e2e
path: bin/coverage/e2e/
if-no-files-found: error
-
name: Test standalone mode
if: ${{ matrix.mode == 'standalone' }}
@@ -196,9 +203,44 @@ jobs:
if: ${{ matrix.mode == 'cucumber'}}
run: |
make test-cucumber
-
name: Upload coverage to Codecov
coverage:
runs-on: ubuntu-22.04
needs:
- test
- e2e
steps:
# codecov won't process the report without the source code available
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: 'go.mod'
check-latest: true
- name: Download unit test coverage
uses: actions/download-artifact@v3
with:
name: coverage-data-unit
path: coverage/unit
- name: Download E2E test coverage
uses: actions/download-artifact@v3
with:
name: coverage-data-e2e
path: coverage/e2e
- name: Merge coverage reports
run: |
go tool covdata textfmt -i=./coverage/unit,./coverage/e2e -o ./coverage.txt
- name: Store coverage report in GitHub Actions
uses: actions/upload-artifact@v3
with:
name: go-covdata-txt
path: ./coverage.txt
if-no-files-found: error
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage.txt
release:
permissions:
@@ -216,10 +258,10 @@ jobs:
uses: actions/download-artifact@v3
with:
name: compose
path: ${{ env.DESTDIR }}
path: bin/release
-
name: Create checksums
working-directory: ${{ env.DESTDIR }}
working-directory: bin/release
run: |
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
@@ -227,21 +269,21 @@ jobs:
cat checksums.txt | while read sum file; do echo "$sum $file" > ${file#\*}.sha256; done
-
name: License
run: cp packaging/* ${{ env.DESTDIR }}/
run: cp packaging/* bin/release/
-
name: List artifacts
run: |
tree -nh ${{ env.DESTDIR }}
tree -nh bin/release
-
name: Check artifacts
run: |
find ${{ env.DESTDIR }} -type f -exec file -e ascii -- {} +
find bin/release -type f -exec file -e ascii -- {} +
-
name: GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37 # v1.10.0
with:
artifacts: ${{ env.DESTDIR }}/*
artifacts: bin/release/*
generateReleaseNotes: true
draft: true
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -76,6 +76,8 @@ jobs:
bin-image:
runs-on: ubuntu-22.04
outputs:
digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }}
steps:
-
name: Checkout
@@ -107,6 +109,7 @@ jobs:
-
name: Build and push image
uses: docker/bake-action@v2
id: bake
with:
files: |
./docker-bake.hcl
@@ -118,3 +121,31 @@ jobs:
*.cache-to=type=gha,scope=bin-image,mode=max
*.attest=type=sbom
*.attest=type=provenance,mode=max,builder-id=https://github.com/${{ env.GITHUB_REPOSITORY }}/actions/runs/${{ env.GITHUB_RUN_ID }}
desktop-edge-test:
runs-on: ubuntu-latest
needs: bin-image
steps:
-
name: Generate Token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ vars.DOCKERDESKTOP_APP_ID }}
private_key: ${{ secrets.DOCKERDESKTOP_APP_PRIVATEKEY }}
repository: docker/${{ secrets.DOCKERDESKTOP_REPO }}
-
name: Trigger Docker Desktop e2e with edge version
uses: actions/github-script@v6
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'docker',
repo: '${{ secrets.DOCKERDESKTOP_REPO }}',
workflow_id: 'compose-edge-integration.yml',
ref: 'main',
inputs: {
"image-tag": "${{ needs.bin-image.outputs.digest }}"
}
})

View File

@@ -31,12 +31,11 @@ linters-settings:
- name: package-comments
disabled: true
depguard:
list-type: denylist
include-go-root: true
packages:
# The io/ioutil package has been deprecated.
# https://go.dev/doc/go1.16#ioutil
- io/ioutil
rules:
all:
deny:
- pkg: io/ioutil
desc: 'io/ioutil package has been deprecated'
gomodguard:
blocked:
versions:

View File

@@ -15,9 +15,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
ARG GO_VERSION=1.20.4
ARG GO_VERSION=1.20.5
ARG XX_VERSION=1.2.1
ARG GOLANGCI_LINT_VERSION=v1.52.2
ARG GOLANGCI_LINT_VERSION=v1.53.2
ARG ADDLICENSE_VERSION=v1.0.0
ARG BUILD_TAGS="e2e"
@@ -84,8 +84,8 @@ RUN --mount=type=bind,target=. \
--mount=type=bind,from=osxcross,src=/osxsdk,target=/xx-sdk \
xx-go --wrap && \
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; fi && \
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/usr/bin && \
xx-verify --static /usr/bin/docker-compose
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/out && \
xx-verify --static /out/docker-compose
FROM build-base AS lint
ARG BUILD_TAGS
@@ -100,11 +100,13 @@ ARG BUILD_TAGS
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
go test -tags "$BUILD_TAGS" -v -coverprofile=/tmp/coverage.txt -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') && \
go tool cover -func=/tmp/coverage.txt
rm -rf /tmp/coverage && \
mkdir -p /tmp/coverage && \
go test -tags "$BUILD_TAGS" -v -cover -covermode=atomic $(go list $(TAGS) ./... | grep -vE 'e2e') -args -test.gocoverdir="/tmp/coverage" && \
go tool covdata percent -i=/tmp/coverage
FROM scratch AS test-coverage
COPY --from=test /tmp/coverage.txt /coverage.txt
COPY --from=test --link /tmp/coverage /
FROM base AS license-set
ARG LICENSE_FILES
@@ -162,11 +164,11 @@ RUN --mount=target=/context \
EOT
FROM scratch AS binary-unix
COPY --link --from=build /usr/bin/docker-compose /
COPY --link --from=build /out/docker-compose /
FROM binary-unix AS binary-darwin
FROM binary-unix AS binary-linux
FROM scratch AS binary-windows
COPY --link --from=build /usr/bin/docker-compose /docker-compose.exe
COPY --link --from=build /out/docker-compose /docker-compose.exe
FROM binary-$TARGETOS AS binary
# enable scanning for this stage
ARG BUILDKIT_SBOM_SCAN_STAGE=true

View File

@@ -17,29 +17,18 @@ VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
GO_BUILDTAGS ?= e2e
DRIVE_PREFIX?=
ifeq ($(OS),Windows_NT)
DETECTED_OS = Windows
DRIVE_PREFIX=C:
else
DETECTED_OS = $(shell uname -s)
endif
ifeq ($(DETECTED_OS),Linux)
MOBY_DOCKER=/usr/bin/docker
endif
ifeq ($(DETECTED_OS),Darwin)
MOBY_DOCKER=/Applications/Docker.app/Contents/Resources/bin/docker
endif
ifeq ($(DETECTED_OS),Windows)
BINARY_EXT=.exe
endif
TEST_COVERAGE_FLAGS = -coverprofile=coverage.out -covermode=atomic
ifneq ($(DETECTED_OS),Windows)
# go race detector requires gcc on Windows so not used by default
# https://github.com/golang/go/issues/27089
TEST_COVERAGE_FLAGS += -race
endif
BUILD_FLAGS?=
TEST_FLAGS?=
E2E_TEST?=
@@ -49,13 +38,23 @@ else
endif
BUILDX_CMD ?= docker buildx
DESTDIR ?= ./bin/build
# DESTDIR overrides the output path for binaries and other artifacts
# this is used by docker/docker-ce-packaging for the apt/rpm builds,
# so it's important that the resulting binary ends up EXACTLY at the
# path $DESTDIR/docker-compose when specified.
#
# See https://github.com/docker/docker-ce-packaging/blob/e43fbd37e48fde49d907b9195f23b13537521b94/rpm/SPECS/docker-compose-plugin.spec#L47
#
# By default, all artifacts go to subdirectories under ./bin/ in the
# repo root, e.g. ./bin/build, ./bin/coverage, ./bin/release.
DESTDIR ?=
all: build
.PHONY: build ## Build the compose cli-plugin
build:
GO111MODULE=on go build $(BUILD_FLAGS) -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(DESTDIR)/docker-compose$(BINARY_EXT)" ./cmd
GO111MODULE=on go build $(BUILD_FLAGS) -trimpath -tags "$(GO_BUILDTAGS)" -ldflags "$(GO_LDFLAGS)" -o "$(or $(DESTDIR),./bin/build)/docker-compose$(BINARY_EXT)" ./cmd
.PHONY: binary
binary:
@@ -68,7 +67,7 @@ binary-with-coverage:
.PHONY: install
install: binary
mkdir -p ~/.docker/cli-plugins
install bin/build/docker-compose ~/.docker/cli-plugins/docker-compose
install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose
.PHONY: e2e-compose
e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
@@ -122,8 +121,8 @@ docs: ## generate documentation
$(eval $@_TMP_OUT := $(shell mktemp -d -t compose-output.XXXXXXXXXX))
$(BUILDX_CMD) bake --set "*.output=type=local,dest=$($@_TMP_OUT)" docs-update
rm -rf ./docs/internal
cp -R "$($@_TMP_OUT)"/out/* ./docs/
rm -rf "$($@_TMP_OUT)"/*
cp -R "$(DRIVE_PREFIX)$($@_TMP_OUT)"/out/* ./docs/
rm -rf "$(DRIVE_PREFIX)$($@_TMP_OUT)"/*
.PHONY: validate-docs
validate-docs: ## validate the doc does not change

131
cmd/cmdtrace/cmd_span.go Normal file
View File

@@ -0,0 +1,131 @@
/*
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 cmdtrace
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"time"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/docker/compose/v2/internal/tracing"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// Setup should be called as part of the command's PersistentPreRunE
// as soon as possible after initializing the dockerCli.
//
// It initializes the tracer for the CLI using both auto-detection
// from the Docker context metadata as well as standard OTEL_ env
// vars, creates a root span for the command, and wraps the actual
// command invocation to ensure the span is properly finalized and
// exported before exit.
func Setup(cmd *cobra.Command, dockerCli command.Cli) error {
tracingShutdown, err := tracing.InitTracing(dockerCli)
if err != nil {
return fmt.Errorf("initializing tracing: %w", err)
}
ctx := cmd.Context()
ctx, cmdSpan := tracing.Tracer.Start(
ctx,
"cli/"+strings.Join(commandName(cmd), "-"),
)
cmd.SetContext(ctx)
wrapRunE(cmd, cmdSpan, tracingShutdown)
return nil
}
// wrapRunE injects a wrapper function around the command's actual RunE (or Run)
// method. This is necessary to capture the command result for reporting as well
// as flushing any spans before exit.
//
// Unfortunately, PersistentPostRun(E) can't be used for this purpose because it
// only runs if RunE does _not_ return an error, but this should run unconditionally.
func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.ShutdownFunc) {
origRunE := c.RunE
if origRunE == nil {
origRun := c.Run
//nolint:unparam // wrapper function for RunE, always returns nil by design
origRunE = func(cmd *cobra.Command, args []string) error {
origRun(cmd, args)
return nil
}
c.Run = nil
}
c.RunE = func(cmd *cobra.Command, args []string) error {
cmdErr := origRunE(cmd, args)
if cmdSpan != nil {
if cmdErr != nil && !errors.Is(cmdErr, context.Canceled) {
// default exit code is 1 if a more descriptive error
// wasn't returned
exitCode := 1
var statusErr dockercli.StatusError
if errors.As(cmdErr, &statusErr) {
exitCode = statusErr.StatusCode
}
cmdSpan.SetStatus(codes.Error, "CLI command returned error")
cmdSpan.RecordError(cmdErr, trace.WithAttributes(
attribute.Int("exit_code", exitCode),
))
} else {
cmdSpan.SetStatus(codes.Ok, "")
}
cmdSpan.End()
}
if tracingShutdown != nil {
// use background for root context because the cmd's context might have
// been canceled already
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// TODO(milas): add an env var to enable logging from the
// OTel components for debugging purposes
_ = tracingShutdown(ctx)
}
return cmdErr
}
}
// commandName returns the path components for a given command.
//
// The root Compose command and anything before (i.e. "docker")
// are not included.
//
// For example:
// - docker compose alpha watch -> [alpha, watch]
// - docker-compose up -> [up]
func commandName(cmd *cobra.Command) []string {
var name []string
for c := cmd; c != nil; c = c.Parent() {
if c.Name() == commands.PluginName {
break
}
name = append(name, c.Name())
}
sort.Sort(sort.Reverse(sort.StringSlice(name)))
return name
}

View File

@@ -62,10 +62,6 @@ func Convert(args []string) []string {
continue
}
if len(arg) > 0 && arg[0] != '-' {
// not a top-level flag anymore, keep the rest of the command unmodified
if arg == compose.PluginName {
i++
}
command = append(command, args[i:]...)
break
}

View File

@@ -83,6 +83,11 @@ func Test_convert(t *testing.T) {
args: []string{"--project-directory", "", "ps"},
want: []string{"compose", "--project-directory", "", "ps"},
},
{
name: "compose as project name",
args: []string{"--project-name", "compose", "down", "--remove-orphans"},
want: []string{"compose", "--project-name", "compose", "down", "--remove-orphans"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -27,8 +27,7 @@ import (
"github.com/compose-spec/compose-go/types"
buildx "github.com/docker/buildx/util/progress"
cliopts "github.com/docker/cli/opts"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
@@ -37,14 +36,14 @@ import (
type buildOptions struct {
*ProjectOptions
composeOptions
quiet bool
pull bool
push bool
progress string
args []string
noCache bool
memory cliopts.MemBytes
ssh string
quiet bool
pull bool
push bool
args []string
noCache bool
memory cliopts.MemBytes
ssh string
builder string
}
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
@@ -56,27 +55,25 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
return api.BuildOptions{}, err
}
}
builderName := opts.builder
if builderName == "" {
builderName = os.Getenv("BUILDX_BUILDER")
}
return api.BuildOptions{
Pull: opts.pull,
Push: opts.push,
Progress: opts.progress,
Progress: ui.Mode,
Args: types.NewMappingWithEquals(opts.args),
NoCache: opts.noCache,
Quiet: opts.quiet,
Services: services,
SSHs: SSHKeys,
Builder: builderName,
}, nil
}
var printerModes = []string{
buildx.PrinterModeAuto,
buildx.PrinterModeTty,
buildx.PrinterModePlain,
buildx.PrinterModeQuiet,
}
func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
func buildCommand(p *ProjectOptions, progress *string, backend api.Service) *cobra.Command {
opts := buildOptions{
ProjectOptions: p,
}
@@ -85,24 +82,21 @@ func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
Short: "Build or rebuild services",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet {
opts.progress = buildx.PrinterModeQuiet
ui.Mode = ui.ModeQuiet
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
}
os.Stdout = devnull
}
if !utils.StringContains(printerModes, opts.progress) {
return fmt.Errorf("unsupported --progress value %q", opts.progress)
}
return nil
}),
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("ssh") && opts.ssh == "" {
opts.ssh = "default"
}
if progress.Mode == progress.ModePlain && !cmd.Flags().Changed("progress") {
opts.progress = buildx.PrinterModePlain
if cmd.Flags().Changed("progress") && opts.ssh == "" {
fmt.Fprint(os.Stderr, "--progress is a global compose flag, better use `docker compose --progress xx build ...")
}
return runBuild(ctx, backend, opts, args)
}),
@@ -111,9 +105,9 @@ func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
cmd.Flags().BoolVar(&opts.push, "push", false, "Push service images.")
cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.")
cmd.Flags().StringVar(&opts.progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
cmd.Flags().StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services.")
cmd.Flags().StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)")
cmd.Flags().StringVar(&opts.builder, "builder", "", "Set builder to use.")
cmd.Flags().Bool("parallel", true, "Build images in parallel. DEPRECATED")
cmd.Flags().MarkHidden("parallel") //nolint:errcheck
cmd.Flags().Bool("compress", true, "Compress the build context using gzip. DEPRECATED")
@@ -124,6 +118,8 @@ func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
cmd.Flags().Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
cmd.Flags().MarkHidden("no-rm") //nolint:errcheck
cmd.Flags().VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.")
cmd.Flags().StringVar(progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
cmd.Flags().MarkHidden("progress") //nolint:errcheck
return cmd
}

View File

@@ -26,6 +26,7 @@ import (
"strings"
"syscall"
buildx "github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/compose-spec/compose-go/cli"
@@ -43,7 +44,7 @@ import (
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v2/pkg/progress"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
)
@@ -175,7 +176,7 @@ func (o *ProjectOptions) toProjectName() (string, error) {
return o.ProjectName, nil
}
envProjectName := os.Getenv("ComposeProjectName")
envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" {
return envProjectName, nil
}
@@ -273,6 +274,7 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
version bool
parallel int
dryRun bool
progress string
)
c := &cobra.Command{
Short: "Docker Compose",
@@ -326,16 +328,36 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
formatter.SetANSIMode(streams, ansi)
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
progress.NoColor()
ui.NoColor()
formatter.SetANSIMode(streams, formatter.Never)
}
switch ansi {
case "never":
progress.Mode = progress.ModePlain
ui.Mode = ui.ModePlain
case "always":
progress.Mode = progress.ModeTTY
ui.Mode = ui.ModeTTY
}
switch progress {
case ui.ModeAuto:
ui.Mode = ui.ModeAuto
case ui.ModeTTY:
if ansi == "never" {
return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
}
ui.Mode = ui.ModeTTY
case ui.ModePlain:
if ansi == "always" {
return fmt.Errorf("can't use --progress plain while ANSI support is forced")
}
ui.Mode = ui.ModePlain
case ui.ModeQuiet, "none":
ui.Mode = ui.ModeQuiet
default:
return fmt.Errorf("unsupported --progress value %q", progress)
}
if opts.WorkDir != "" {
if opts.ProjectDir != "" {
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
@@ -404,11 +426,13 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
portCommand(&opts, streams, backend),
imagesCommand(&opts, streams, backend),
versionCommand(streams),
buildCommand(&opts, backend),
buildCommand(&opts, &progress, backend),
publishCommand(&opts, backend),
pushCommand(&opts, backend),
pullCommand(&opts, backend),
createCommand(&opts, backend),
copyCommand(&opts, backend),
waitCommand(&opts, backend),
alphaCommand(&opts, backend),
)
@@ -425,6 +449,8 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
},
)
c.Flags().StringVar(&progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
c.Flags().IntVar(&parallel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
@@ -460,3 +486,10 @@ func setEnvWithDotEnv(prjOpts *ProjectOptions) error {
}
return nil
}
var printerModes = []string{
ui.ModeAuto,
ui.ModeTTY,
ui.ModePlain,
ui.ModeQuiet,
}

View File

@@ -44,7 +44,7 @@ func downCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
ProjectOptions: p,
}
downCmd := &cobra.Command{
Use: "down [OPTIONS]",
Use: "down [OPTIONS] [SERVICES]",
Short: "Stop and remove containers, networks",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.timeChanged = cmd.Flags().Changed("timeout")
@@ -56,16 +56,15 @@ func downCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runDown(ctx, backend, opts)
return runDown(ctx, backend, opts, args)
}),
Args: cobra.NoArgs,
ValidArgsFunction: noCompletion(),
}
flags := downCmd.Flags()
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.")
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
flags.BoolVarP(&opts.volumes, "volumes", "v", false, "Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers.")
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
flags.BoolVarP(&opts.volumes, "volumes", "v", false, `Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers.`)
flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "volume" {
@@ -77,7 +76,7 @@ func downCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
return downCmd
}
func runDown(ctx context.Context, backend api.Service, opts downOptions) error {
func runDown(ctx context.Context, backend api.Service, opts downOptions, services []string) error {
project, name, err := opts.projectOrName()
if err != nil {
return err
@@ -94,5 +93,6 @@ func runDown(ctx context.Context, backend api.Service, opts downOptions) error {
Timeout: timeout,
Images: opts.images,
Volumes: opts.volumes,
Services: services,
})
}

55
cmd/compose/publish.go Normal file
View File

@@ -0,0 +1,55 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type publishOptions struct {
*ProjectOptions
composeOptions
Repository string
}
func publishCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
opts := pushOptions{
ProjectOptions: p,
}
publishCmd := &cobra.Command{
Use: "publish [OPTIONS] [REPOSITORY]",
Short: "Publish compose application",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPublish(ctx, backend, opts, args[0])
}),
Args: cobra.ExactArgs(1),
}
return publishCmd
}
func runPublish(ctx context.Context, backend api.Service, opts pushOptions, repository string) error {
project, err := opts.ToProject(nil)
if err != nil {
return err
}
return backend.Publish(ctx, project, repository)
}

View File

@@ -31,6 +31,7 @@ type pushOptions struct {
IncludeDeps bool
Ignorefailures bool
Quiet bool
Repository string
}
func pushCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
@@ -48,6 +49,7 @@ func pushCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
pushCmd.Flags().BoolVar(&opts.IncludeDeps, "include-deps", false, "Also push images of services declared as dependencies")
pushCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Push without printing progress information")
pushCmd.Flags().StringVarP(&opts.Repository, "repository", "r", "", "Also publish the compose application in repository")
return pushCmd
}
@@ -68,5 +70,6 @@ func runPush(ctx context.Context, backend api.Service, opts pushOptions, service
return backend.Push(ctx, project, api.PushOptions{
IgnoreFailures: opts.Ignorefailures,
Quiet: opts.Quiet,
Repository: opts.Repository,
})
}

View File

@@ -28,8 +28,9 @@ import (
type restartOptions struct {
*ProjectOptions
timeout int
noDeps bool
timeChanged bool
timeout int
noDeps bool
}
func restartCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
@@ -39,13 +40,16 @@ func restartCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
restartCmd := &cobra.Command{
Use: "restart [OPTIONS] [SERVICE...]",
Short: "Restart service containers",
PreRun: func(cmd *cobra.Command, args []string) {
opts.timeChanged = cmd.Flags().Changed("timeout")
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runRestart(ctx, backend, opts, args)
}),
ValidArgsFunction: completeServiceNames(p),
}
flags := restartCmd.Flags()
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't restart dependent services.")
return restartCmd
@@ -57,6 +61,12 @@ func runRestart(ctx context.Context, backend api.Service, opts restartOptions, s
return err
}
var timeout *time.Duration
if opts.timeChanged {
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
if opts.noDeps {
err := project.ForServices(services, types.IgnoreDependencies)
if err != nil {
@@ -64,9 +74,8 @@ func runRestart(ctx context.Context, backend api.Service, opts restartOptions, s
}
}
timeout := time.Duration(opts.timeout) * time.Second
return backend.Restart(ctx, name, api.RestartOptions{
Timeout: &timeout,
Timeout: timeout,
Services: services,
Project: project,
})

View File

@@ -24,6 +24,7 @@ import (
cgo "github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/opts"
"github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -48,6 +49,8 @@ type runOptions struct {
workdir string
entrypoint string
entrypointCmd []string
capAdd opts.ListOpts
capDrop opts.ListOpts
labels []string
volumes []string
publish []string
@@ -59,20 +62,20 @@ type runOptions struct {
quietPull bool
}
func (opts runOptions) apply(project *types.Project) error {
target, err := project.GetService(opts.Service)
func (options runOptions) apply(project *types.Project) error {
target, err := project.GetService(options.Service)
if err != nil {
return err
}
target.Tty = !opts.noTty
target.StdinOpen = opts.interactive
if !opts.servicePorts {
target.Tty = !options.noTty
target.StdinOpen = options.interactive
if !options.servicePorts {
target.Ports = []types.ServicePortConfig{}
}
if len(opts.publish) > 0 {
if len(options.publish) > 0 {
target.Ports = []types.ServicePortConfig{}
for _, p := range opts.publish {
for _, p := range options.publish {
config, err := types.ParsePortConfig(p)
if err != nil {
return err
@@ -80,8 +83,8 @@ func (opts runOptions) apply(project *types.Project) error {
target.Ports = append(target.Ports, config...)
}
}
if len(opts.volumes) > 0 {
for _, v := range opts.volumes {
if len(options.volumes) > 0 {
for _, v := range options.volumes {
volume, err := loader.ParseVolume(v)
if err != nil {
return err
@@ -90,15 +93,15 @@ func (opts runOptions) apply(project *types.Project) error {
}
}
if opts.noDeps {
err := project.ForServices([]string{opts.Service}, types.IgnoreDependencies)
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 == opts.Service {
if s.Name == options.Service {
project.Services[i] = target
break
}
@@ -107,10 +110,12 @@ func (opts runOptions) apply(project *types.Project) error {
}
func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
opts := runOptions{
options := runOptions{
composeOptions: &composeOptions{
ProjectOptions: p,
},
capAdd: opts.NewListOpts(nil),
capDrop: opts.NewListOpts(nil),
}
createOpts := createOptions{}
cmd := &cobra.Command{
@@ -118,61 +123,63 @@ func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *co
Short: "Run a one-off command on a service.",
Args: cobra.MinimumNArgs(1),
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.Service = args[0]
options.Service = args[0]
if len(args) > 1 {
opts.Command = args[1:]
options.Command = args[1:]
}
if len(opts.publish) > 0 && opts.servicePorts {
if len(options.publish) > 0 && options.servicePorts {
return fmt.Errorf("--service-ports and --publish are incompatible")
}
if cmd.Flags().Changed("entrypoint") {
command, err := shellwords.Parse(opts.entrypoint)
command, err := shellwords.Parse(options.entrypoint)
if err != nil {
return err
}
opts.entrypointCmd = command
options.entrypointCmd = command
}
if cmd.Flags().Changed("tty") {
if cmd.Flags().Changed("no-TTY") {
return fmt.Errorf("--tty and --no-TTY can't be used together")
} else {
opts.noTty = !opts.tty
options.noTty = !options.tty
}
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
project, err := p.ToProject([]string{opts.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile)
project, err := p.ToProject([]string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile)
if err != nil {
return err
}
opts.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
return runRun(ctx, backend, project, opts, createOpts, streams)
options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
return runRun(ctx, backend, project, options, createOpts, streams)
}),
ValidArgsFunction: completeServiceNames(p),
}
flags := cmd.Flags()
flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
flags.StringArrayVarP(&opts.labels, "label", "l", []string{}, "Add or override a label")
flags.BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits")
flags.BoolVarP(&opts.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).")
flags.StringVar(&opts.name, "name", "", "Assign a name to the container")
flags.StringVarP(&opts.user, "user", "u", "", "Run as specified username or uid")
flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
flags.StringVar(&opts.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.")
flags.StringArrayVarP(&opts.volumes, "volume", "v", []string{}, "Bind mount a volume.")
flags.StringArrayVarP(&opts.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
flags.BoolVar(&opts.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.")
flags.BoolVar(&opts.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.")
flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information.")
flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID")
flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables")
flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label")
flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits")
flags.BoolVarP(&options.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).")
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid")
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities")
flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities")
flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services.")
flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume.")
flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.")
flags.BoolVar(&options.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.")
flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information.")
flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container.")
flags.BoolVar(&createOpts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
cmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
cmd.Flags().BoolVarP(&opts.tty, "tty", "t", true, "Allocate a pseudo-TTY.")
cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY.")
cmd.Flags().MarkHidden("tty") //nolint:errcheck
flags.SetNormalizeFunc(normalizeRunFlags)
@@ -190,8 +197,8 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
return pflag.NormalizedName(name)
}
func runRun(ctx context.Context, backend api.Service, project *types.Project, opts runOptions, createOpts createOptions, streams api.Streams) error {
err := opts.apply(project)
func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, streams api.Streams) error {
err := options.apply(project)
if err != nil {
return err
}
@@ -202,14 +209,14 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
}
err = progress.Run(ctx, func(ctx context.Context) error {
return startDependencies(ctx, backend, *project, opts.Service, opts.ignoreOrphans)
return startDependencies(ctx, backend, *project, options.Service, options.ignoreOrphans)
}, streams.Err())
if err != nil {
return err
}
labels := types.Labels{}
for _, s := range opts.labels {
for _, s := range options.labels {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("label must be set as KEY=VALUE")
@@ -219,27 +226,29 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
// start container and attach to container streams
runOpts := api.RunOptions{
Name: opts.name,
Service: opts.Service,
Command: opts.Command,
Detach: opts.Detach,
AutoRemove: opts.Remove,
Tty: !opts.noTty,
Interactive: opts.interactive,
WorkingDir: opts.workdir,
User: opts.user,
Environment: opts.environment,
Entrypoint: opts.entrypointCmd,
Name: options.name,
Service: options.Service,
Command: options.Command,
Detach: options.Detach,
AutoRemove: options.Remove,
Tty: !options.noTty,
Interactive: options.interactive,
WorkingDir: options.workdir,
User: options.user,
CapAdd: options.capAdd.GetAll(),
CapDrop: options.capDrop.GetAll(),
Environment: options.environment,
Entrypoint: options.entrypointCmd,
Labels: labels,
UseNetworkAliases: opts.useAliases,
NoDeps: opts.noDeps,
UseNetworkAliases: options.useAliases,
NoDeps: options.noDeps,
Index: 0,
QuietPull: opts.quietPull,
QuietPull: options.quietPull,
}
for i, service := range project.Services {
if service.Name == opts.Service {
service.StdinOpen = opts.interactive
if service.Name == options.Service {
service.StdinOpen = options.interactive
project.Services[i] = service
}
}

View File

@@ -47,7 +47,7 @@ func stopCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
ValidArgsFunction: completeServiceNames(p),
}
flags := cmd.Flags()
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
flags.IntVarP(&opts.timeout, "timeout", "t", 0, "Specify a shutdown timeout in seconds")
return cmd
}

View File

@@ -78,7 +78,7 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
Short: "Create and start containers",
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
create.pullChanged = cmd.Flags().Changed("pull")
create.timeChanged = cmd.Flags().Changed("waitTimeout")
create.timeChanged = cmd.Flags().Changed("timeout")
return validateFlags(&up, &create)
}),
RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
@@ -104,7 +104,7 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
flags.BoolVar(&up.noStart, "no-start", false, "Don't start the services after creating them.")
flags.BoolVar(&up.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
flags.StringVar(&up.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
flags.IntVarP(&create.timeout, "timeout", "t", 10, "Use this timeout in seconds for container shutdown when attached or when containers are already running.")
flags.IntVarP(&create.timeout, "timeout", "t", 0, "Use this timeout in seconds for container shutdown when attached or when containers are already running.")
flags.BoolVar(&up.timestamp, "timestamps", false, "Show timestamps.")
flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services.")
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")

72
cmd/compose/wait.go Normal file
View File

@@ -0,0 +1,72 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"os"
"github.com/docker/cli/cli"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type waitOptions struct {
*ProjectOptions
services []string
downProject bool
}
func waitCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
opts := waitOptions{
ProjectOptions: p,
}
var statusCode int64
var err error
cmd := &cobra.Command{
Use: "wait SERVICE [SERVICE...] [OPTIONS]",
Short: "Block until the first service container stops",
Args: cli.RequiresMinArgs(1),
RunE: Adapt(func(ctx context.Context, services []string) error {
opts.services = services
statusCode, err = runWait(ctx, backend, &opts)
return err
}),
PostRun: func(cmd *cobra.Command, args []string) {
os.Exit(int(statusCode))
},
}
cmd.Flags().BoolVar(&opts.downProject, "down-project", false, "Drops project when the first container stops")
return cmd
}
func runWait(ctx context.Context, backend api.Service, opts *waitOptions) (int64, error) {
_, name, err := opts.projectOrName()
if err != nil {
return 0, err
}
return backend.Wait(ctx, name, api.WaitOptions{
Services: opts.services,
DownProjectOnContainerExit: opts.downProject,
})
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/cmdtrace"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/compatibility"
@@ -38,14 +39,20 @@ func pluginMain() {
cmd := commands.RootCommand(dockerCli, serviceProxy)
originalPreRun := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// initialize the dockerCli instance
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
// TODO(milas): add an env var to enable logging from the
// OTel components for debugging purposes
_ = cmdtrace.Setup(cmd, dockerCli)
if originalPreRun != nil {
return originalPreRun(cmd, args)
}
return nil
}
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
return dockercli.StatusError{
StatusCode: compose.CommandSyntaxFailure.ExitCode,

21
codecov.yml Normal file
View File

@@ -0,0 +1,21 @@
coverage:
status:
project:
default:
informational: true
target: auto
threshold: 2%
patch:
default:
informational: true
comment:
require_changes: true
ignore:
- "packaging"
- "docs"
- "bin"
- "e2e"
- "pkg/e2e"
- "**/*_test.go"

View File

@@ -25,13 +25,16 @@ variable "DOCS_FORMATS" {
default = "md,yaml"
}
# Defines the output folder
# Defines the output folder to override the default behavior.
# See Makefile for details, this is generally only useful for
# the packaging scripts and care should be taken to not break
# them.
variable "DESTDIR" {
default = ""
}
function "bindir" {
function "outdir" {
params = [defaultdir]
result = DESTDIR != "" ? DESTDIR : "./bin/${defaultdir}"
result = DESTDIR != "" ? DESTDIR : "${defaultdir}"
}
# Special target: https://github.com/docker/metadata-action#bake-definition
@@ -84,23 +87,23 @@ target "vendor-update" {
target "test" {
inherits = ["_common"]
target = "test-coverage"
output = [bindir("coverage")]
output = [outdir("./bin/coverage/unit")]
}
target "binary-with-coverage" {
inherits = ["_common"]
target = "binary"
args = {
BUILD_FLAGS = "-cover"
BUILD_FLAGS = "-cover -covermode=atomic"
}
output = [bindir("build")]
output = [outdir("./bin/build")]
platforms = ["local"]
}
target "binary" {
inherits = ["_common"]
target = "binary"
output = [bindir("build")]
output = [outdir("./bin/build")]
platforms = ["local"]
}
@@ -124,7 +127,7 @@ target "binary-cross" {
target "release" {
inherits = ["binary-cross"]
target = "release"
output = [bindir("release")]
output = [outdir("./bin/release")]
}
target "docs-validate" {

View File

@@ -33,6 +33,7 @@ Define and run multi-container applications with Docker.
| [`unpause`](compose_unpause.md) | Unpause services |
| [`up`](compose_up.md) | Create and start containers |
| [`version`](compose_version.md) | Show the Docker Compose version information |
| [`wait`](compose_wait.md) | Block until the first service container stops |
### Options
@@ -46,6 +47,7 @@ Define and run multi-container applications with Docker.
| `-f`, `--file` | `stringArray` | | Compose configuration files |
| `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited |
| `--profile` | `stringArray` | | Specify a profile to enable |
| `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, quiet) |
| `--project-directory` | `string` | | Specify an alternate working directory<br>(default: the path of the, first specified, Compose file) |
| `-p`, `--project-name` | `string` | | Project name |
@@ -171,7 +173,6 @@ If flags are explicitly set on the command line, the associated environment vari
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` will stop docker compose from detecting orphaned
containers for the project.
### Use Dry Run mode to test your command
Use `--dry-run` flag to test a command without changing your application stack state.
@@ -195,24 +196,4 @@ $ docker compose --dry-run up --build -d
From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service.
Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting.
Dry Run mode does not currently work with all commands. In particular, you cannot use Dry Run mode with a command that doesn't change the state of a Compose stack
such as `ps`, `ls`, `logs` for example.
Here the list of commands supporting `--dry-run` flag:
* build
* cp
* create
* down
* exec
* kill
* pause
* pull
* push
* remove
* restart
* run
* start
* stop
* unpause
* up
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.

View File

@@ -8,10 +8,10 @@ Build or rebuild services
| Name | Type | Default | Description |
|:-----------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------|
| `--build-arg` | `stringArray` | | Set build-time variables for services. |
| `--builder` | `string` | | Set builder to use. |
| `--dry-run` | | | Execute command in dry run mode |
| `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. |
| `--no-cache` | | | Do not use cache when building the image |
| `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, quiet) |
| `--pull` | | | Always attempt to pull a newer version of the image. |
| `--push` | | | Push service images. |
| `-q`, `--quiet` | | | Don't print anything to STDOUT |

View File

@@ -10,8 +10,8 @@ Stop and remove containers, networks
| `--dry-run` | | | Execute command in dry run mode |
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. |
| `--rmi` | `string` | | Remove images used by services. "local" remove only images that don't have a custom tag ("local"\|"all") |
| `-t`, `--timeout` | `int` | `10` | Specify a shutdown timeout in seconds |
| `-v`, `--volumes` | | | Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers. |
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
| `-v`, `--volumes` | | | Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers. |
<!---MARKER_GEN_END-->

View File

@@ -9,7 +9,7 @@ Restart service containers
|:------------------|:------|:--------|:--------------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
| `--no-deps` | | | Don't restart dependent services. |
| `-t`, `--timeout` | `int` | `10` | Specify a shutdown timeout in seconds |
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
<!---MARKER_GEN_END-->

View File

@@ -8,6 +8,8 @@ Run a one-off command on a service.
| Name | Type | Default | Description |
|:----------------------|:--------------|:--------|:----------------------------------------------------------------------------------|
| `--build` | | | Build image before starting container. |
| `--cap-add` | `list` | | Add Linux capabilities |
| `--cap-drop` | `list` | | Drop Linux capabilities |
| `-d`, `--detach` | | | Run container in background and print container ID |
| `--dry-run` | | | Execute command in dry run mode |
| `--entrypoint` | `string` | | Override the entrypoint of the image |
@@ -34,7 +36,7 @@ Run a one-off command on a service.
Runs a one-time command against a service.
the following command starts the `web` service and runs `bash` as its command:
The following command starts the `web` service and runs `bash` as its command:
```console
$ docker compose run web bash

View File

@@ -8,7 +8,7 @@ Stop services
| Name | Type | Default | Description |
|:------------------|:------|:--------|:--------------------------------------|
| `--dry-run` | | | Execute command in dry run mode |
| `-t`, `--timeout` | `int` | `10` | Specify a shutdown timeout in seconds |
| `-t`, `--timeout` | `int` | `0` | Specify a shutdown timeout in seconds |
<!---MARKER_GEN_END-->

View File

@@ -28,7 +28,7 @@ Create and start containers
| `--remove-orphans` | | | Remove containers for services not defined in the Compose file. |
| `-V`, `--renew-anon-volumes` | | | Recreate anonymous volumes instead of retrieving data from the previous containers. |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-t`, `--timeout` | `int` | `10` | Use this timeout in seconds for container shutdown when attached or when containers are already running. |
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running. |
| `--timestamps` | | | Show timestamps. |
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
| `--wait-timeout` | `int` | `0` | timeout waiting for application to be running\|healthy. |

View File

@@ -0,0 +1,15 @@
# docker compose wait
<!---MARKER_GEN_START-->
Block until the first service container stops
### Options
| Name | Type | Default | Description |
|:-----------------|:-----|:--------|:---------------------------------------------|
| `--down-project` | | | Drops project when the first container stops |
| `--dry-run` | | | Execute command in dry run mode |
<!---MARKER_GEN_END-->

View File

@@ -118,7 +118,6 @@ long: |-
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` will stop docker compose from detecting orphaned
containers for the project.
### Use Dry Run mode to test your command
Use `--dry-run` flag to test a command without changing your application stack state.
@@ -142,26 +141,7 @@ long: |-
From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service.
Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting.
Dry Run mode does not currently work with all commands. In particular, you cannot use Dry Run mode with a command that doesn't change the state of a Compose stack
such as `ps`, `ls`, `logs` for example.
Here the list of commands supporting `--dry-run` flag:
* build
* cp
* create
* down
* exec
* kill
* pause
* pull
* push
* remove
* restart
* run
* start
* stop
* unpause
* up
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.
usage: docker compose
pname: docker
plink: docker.yaml
@@ -191,6 +171,7 @@ cname:
- docker compose unpause
- docker compose up
- docker compose version
- docker compose wait
clink:
- docker_compose_build.yaml
- docker_compose_config.yaml
@@ -217,6 +198,7 @@ clink:
- docker_compose_unpause.yaml
- docker_compose_up.yaml
- docker_compose_version.yaml
- docker_compose_wait.yaml
options:
- option: ansi
value_type: string
@@ -300,6 +282,16 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: progress
value_type: string
default_value: auto
description: Set type of progress output (auto, tty, plain, quiet)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: project-directory
value_type: string
description: |-

View File

@@ -24,6 +24,15 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: builder
value_type: string
description: Set builder to use.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: compress
value_type: bool
default_value: "true"
@@ -90,9 +99,9 @@ options:
- option: progress
value_type: string
default_value: auto
description: Set type of progress output (auto, tty, plain, quiet)
description: Set type of ui output (auto, tty, plain, quiet)
deprecated: false
hidden: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false

View File

@@ -14,7 +14,7 @@ long: |-
Anonymous volumes are not removed by default. However, as they dont have a stable name, they will not be automatically
mounted by a subsequent `up`. For data that needs to persist between updates, use explicit paths as bind mounts or
named volumes.
usage: docker compose down [OPTIONS]
usage: docker compose down [OPTIONS] [SERVICES]
pname: docker compose
plink: docker_compose.yaml
options:
@@ -41,7 +41,7 @@ options:
- option: timeout
shorthand: t
value_type: int
default_value: "10"
default_value: "0"
description: Specify a shutdown timeout in seconds
deprecated: false
hidden: false
@@ -54,7 +54,7 @@ options:
value_type: bool
default_value: "false"
description: |
Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers.
Remove named volumes declared in the "volumes" section of the Compose file and anonymous volumes attached to containers.
deprecated: false
hidden: false
experimental: false

View File

@@ -28,7 +28,7 @@ options:
- option: timeout
shorthand: t
value_type: int
default_value: "10"
default_value: "0"
description: Specify a shutdown timeout in seconds
deprecated: false
hidden: false

View File

@@ -3,7 +3,7 @@ short: Run a one-off command on a service.
long: |-
Runs a one-time command against a service.
the following command starts the `web` service and runs `bash` as its command:
The following command starts the `web` service and runs `bash` as its command:
```console
$ docker compose run web bash
@@ -68,6 +68,24 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: cap-add
value_type: list
description: Add Linux capabilities
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: cap-drop
value_type: list
description: Drop Linux capabilities
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: detach
shorthand: d
value_type: bool

View File

@@ -9,7 +9,7 @@ options:
- option: timeout
shorthand: t
value_type: int
default_value: "10"
default_value: "0"
description: Specify a shutdown timeout in seconds
deprecated: false
hidden: false

View File

@@ -234,7 +234,7 @@ options:
- option: timeout
shorthand: t
value_type: int
default_value: "10"
default_value: "0"
description: |
Use this timeout in seconds for container shutdown when attached or when containers are already running.
deprecated: false

View File

@@ -0,0 +1,34 @@
command: docker compose wait
short: Block until the first service container stops
long: Block until the first service container stops
usage: docker compose wait SERVICE [SERVICE...] [OPTIONS]
pname: docker compose
plink: docker_compose.yaml
options:
- option: down-project
value_type: bool
default_value: "false"
description: Drops project when the first container stops
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

View File

@@ -0,0 +1,28 @@
Feature: Report port conflicts
Background:
Given a compose file
"""
services:
web:
image: nginx
ports:
- 31415:80
"""
And I run "docker rm -f nginx-pi-31415"
Scenario: Reports a port allocation conflict with another container
Given I run "docker run -d -p 31415:80 --name nginx-pi-31415 nginx"
When I run "compose up -d"
Then the output contains "port is already allocated"
And the exit code is 1
Scenario: Reports a port conflict with some other process
Given a process listening on port 31415
When I run "compose up -d"
Then the output contains "address already in use"
And the exit code is 1
Scenario: Cleanup
Given I run "docker rm -f nginx-pi-31415"

View File

@@ -16,6 +16,7 @@ Background:
"""
FROM golang:1.19-alpine
"""
And I run "docker rm -f external-test"
Scenario: external container from compose image exists
When I run "compose build"
@@ -24,4 +25,5 @@ Scenario: external container from compose image exists
Then the exit code is 0
And I run "compose ps -a"
Then the output does not contain "external-test"
And I run "docker rm -f external-test"

View File

@@ -19,6 +19,7 @@ package cucumber
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"regexp"
@@ -87,6 +88,7 @@ func setup(s *godog.ScenarioContext) {
s.Step(`output contains "(.*)"$`, th.outputContains(true))
s.Step(`output does not contain "(.*)"$`, th.outputContains(false))
s.Step(`exit code is (\d+)$`, th.exitCodeIs)
s.Step(`a process listening on port (\d+)$`, th.listenerOnPort)
}
type testHelper struct {
@@ -174,3 +176,16 @@ func (th *testHelper) setDockerfile(dockerfileString string) error {
}
return nil
}
func (th *testHelper) listenerOnPort(port int) error {
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return err
}
th.T.Cleanup(func() {
_ = l.Close()
})
return nil
}

186
go.mod
View File

@@ -3,103 +3,120 @@ module github.com/docker/compose/v2
go 1.20
require (
github.com/AlecAivazis/survey/v2 v2.3.6
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.13.5
github.com/compose-spec/compose-go v1.15.1
github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.6.21
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-20230327091844-0c958010ace2
github.com/docker/buildx v0.10.4
github.com/docker/cli v23.0.6+incompatible
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 v23.0.6+incompatible
github.com/docker/docker v24.0.2+incompatible
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.5.0
github.com/fsnotify/fsevents v0.1.1
github.com/golang/mock v1.6.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.6.0
github.com/jonboulle/clockwork v0.4.0
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/buildkit v0.11.6
github.com/moby/buildkit v0.11.0-rc3.0.20230609092854-67a08623b95a
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-rc2.0.20221005185240-3a7f492d3f1b
github.com/opencontainers/image-spec v1.1.0-rc4
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.2
github.com/stretchr/testify v1.8.4
github.com/theupdateframework/notary v0.7.0
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
go.opentelemetry.io/otel v1.15.1
go.opentelemetry.io/otel v1.14.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0
go.opentelemetry.io/otel/sdk v1.14.0
go.opentelemetry.io/otel/trace v1.14.0
go.uber.org/goleak v1.2.1
golang.org/x/sync v0.2.0
golang.org/x/sync v0.3.0
google.golang.org/grpc v1.56.0
gopkg.in/yaml.v2 v2.4.0
gotest.tools/v3 v3.4.0
)
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.16.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.15.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.16.4 // indirect
github.com/aws/smithy-go v1.11.2 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.17.6 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.24 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.18.6 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bugsnag/bugsnag-go v1.5.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cloudflare/cfssl v1.4.1
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/ttrpc v1.1.1 // indirect
github.com/containerd/typeurl v1.0.2 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
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
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/fsnotify/fsevents v0.1.1
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gofrs/uuid v4.2.0+incompatible // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/in-toto/in-toto-golang v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/gorm v1.9.11 // indirect
github.com/jonboulle/clockwork v0.4.0
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.12 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
@@ -107,7 +124,6 @@ require (
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.5.0
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
@@ -115,75 +131,63 @@ require (
github.com/moby/sys/symlink v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/runc v1.1.7 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/spf13/afero v1.9.2 // indirect
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-20230105215944-fb433841cbfa // indirect
github.com/tonistiigi/fsutil v0.0.0-20230407161946-9e7a6df48576 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // 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
github.com/zmap/zcrypto v0.0.0-20220605182715-4dfcec6e9a8c // indirect
github.com/zmap/zlint v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1 // indirect
go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
go.opentelemetry.io/otel/metric v0.27.0 // indirect
go.opentelemetry.io/otel/sdk v1.4.1 // indirect
go.opentelemetry.io/otel/trace v1.15.1 // indirect
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
golang.org/x/crypto v0.2.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.40.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.14.0 // indirect
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/net v0.9.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.24.1 // indirect; replaced; see replace for the actual version used
k8s.io/apimachinery v0.24.1 // indirect; replaced; see replace for the actual version used
k8s.io/client-go v0.24.1 // indirect; replaced; see replace for the actual version used
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
k8s.io/api v0.26.2 // indirect
k8s.io/apimachinery v0.26.2 // indirect
k8s.io/client-go v0.26.2 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
oras.land/oras-go/v2 v2.2.0 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace (
// Override for e2e tests
github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7
golang.org/x/oauth2 => golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
// For k8s dependencies, we use a replace directive, to prevent them being
// upgraded to the version specified in containerd, which is not relevant to the
// version needed.
// See https://github.com/docker/buildx/pull/948 for details.
// https://github.com/docker/buildx/blob/v0.9.1/go.mod#L62-L64
k8s.io/api => k8s.io/api v0.22.4
k8s.io/apimachinery => k8s.io/apimachinery v0.22.4
k8s.io/client-go => k8s.io/client-go v0.22.4
)
// Override for e2e tests
replace github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7

698
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
//go:build !windows
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tracing
import (
"context"
"fmt"
"net"
"strings"
"syscall"
)
const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path)
func DialInMemory(ctx context.Context, addr string) (net.Conn, error) {
if !strings.HasPrefix(addr, "unix://") {
return nil, fmt.Errorf("not a Unix socket address: %s", addr)
}
addr = strings.TrimPrefix(addr, "unix://")
if len(addr) > maxUnixSocketPathSize {
//goland:noinspection GoErrorStringFormat
return nil, fmt.Errorf("Unix socket address is too long: %s", addr)
}
var d net.Dialer
return d.DialContext(ctx, "unix", addr)
}

View File

@@ -0,0 +1,35 @@
/*
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 tracing
import (
"context"
"fmt"
"net"
"strings"
"github.com/Microsoft/go-winio"
)
func DialInMemory(ctx context.Context, addr string) (net.Conn, error) {
if !strings.HasPrefix(addr, "npipe://") {
return nil, fmt.Errorf("not a named pipe address: %s", addr)
}
addr = strings.TrimPrefix(addr, "npipe://")
return winio.DialPipeContext(ctx, addr)
}

View File

@@ -0,0 +1,126 @@
/*
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 tracing
import (
"context"
"fmt"
"os"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const otelConfigFieldName = "otel"
// traceClientFromDockerContext creates a gRPC OTLP client based on metadata
// from the active Docker CLI context.
func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptrace.Client, error) {
// attempt to extract an OTEL config from the Docker context to enable
// automatic integration with Docker Desktop;
cfg, err := ConfigFromDockerContext(dockerCli.ContextStore(), dockerCli.CurrentContext())
if err != nil {
return nil, fmt.Errorf("loading otel config from docker context metadata: %v", err)
}
if cfg.Endpoint == "" {
return nil, nil
}
// HACK: unfortunately _all_ public OTEL initialization functions
// implicitly read from the OS env, so temporarily unset them all and
// restore afterwards
defer func() {
for k, v := range otelEnv {
if err := os.Setenv(k, v); err != nil {
panic(fmt.Errorf("restoring env for %q: %v", k, err))
}
}
}()
for k := range otelEnv {
if err := os.Unsetenv(k); err != nil {
return nil, fmt.Errorf("stashing env for %q: %v", k, err)
}
}
dialCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
conn, err := grpc.DialContext(
dialCtx,
cfg.Endpoint,
grpc.WithContextDialer(DialInMemory),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
return nil, fmt.Errorf("initializing otel connection from docker context metadata: %v", err)
}
client := otlptracegrpc.NewClient(otlptracegrpc.WithGRPCConn(conn))
return client, nil
}
// ConfigFromDockerContext inspects extra metadata included as part of the
// specified Docker context to try and extract a valid OTLP client configuration.
func ConfigFromDockerContext(st store.Store, name string) (OTLPConfig, error) {
meta, err := st.GetMetadata(name)
if err != nil {
return OTLPConfig{}, err
}
var otelCfg interface{}
switch m := meta.Metadata.(type) {
case command.DockerContext:
otelCfg = m.AdditionalFields[otelConfigFieldName]
case map[string]interface{}:
otelCfg = m[otelConfigFieldName]
}
if otelCfg == nil {
return OTLPConfig{}, nil
}
otelMap, ok := otelCfg.(map[string]interface{})
if !ok {
return OTLPConfig{}, fmt.Errorf(
"unexpected type for field %q: %T (expected: %T)",
otelConfigFieldName,
otelCfg,
otelMap,
)
}
// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
cfg := OTLPConfig{
Endpoint: valueOrDefault[string](otelMap, "OTEL_EXPORTER_OTLP_ENDPOINT"),
}
return cfg, nil
}
// valueOrDefault returns the type-cast value at the specified key in the map
// if present and the correct type; otherwise, it returns the default value for
// T.
func valueOrDefault[T any](m map[string]interface{}, key string) T {
if v, ok := m[key].(T); ok {
return v
}
return *new(T)
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2020 Docker Compose CLI authors
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.
@@ -14,22 +14,16 @@
limitations under the License.
*/
package compose
package tracing
import (
"github.com/moby/buildkit/util/tracing/detect"
"go.opentelemetry.io/otel"
_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
_ "github.com/moby/buildkit/util/tracing/env" //nolint:blank-imports
)
func init() {
detect.ServiceName = "compose"
// do not log tracing errors to stdio
otel.SetErrorHandler(skipErrors{})
}
// skipErrors is a no-op otel.ErrorHandler.
type skipErrors struct{}
func (skipErrors) Handle(err error) {}
// Handle does nothing, ignoring any errors passed to it.
func (skipErrors) Handle(_ error) {}
var _ otel.ErrorHandler = skipErrors{}

50
internal/tracing/mux.go Normal file
View File

@@ -0,0 +1,50 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tracing
import (
"context"
"github.com/hashicorp/go-multierror"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
type MuxExporter struct {
exporters []sdktrace.SpanExporter
}
func (m MuxExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
var eg multierror.Group
for i := range m.exporters {
exporter := m.exporters[i]
eg.Go(func() error {
return exporter.ExportSpans(ctx, spans)
})
}
return eg.Wait()
}
func (m MuxExporter) Shutdown(ctx context.Context) error {
var eg multierror.Group
for i := range m.exporters {
exporter := m.exporters[i]
eg.Go(func() error {
return exporter.Shutdown(ctx)
})
}
return eg.Wait()
}

156
internal/tracing/tracing.go Normal file
View File

@@ -0,0 +1,156 @@
/*
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"
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/docker/compose/v2/internal"
"go.opentelemetry.io/otel/attribute"
"github.com/docker/cli/cli/command"
"github.com/moby/buildkit/util/tracing/detect"
_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
_ "github.com/moby/buildkit/util/tracing/env" //nolint:blank-imports
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
)
func init() {
detect.ServiceName = "compose"
// do not log tracing errors to stdio
otel.SetErrorHandler(skipErrors{})
}
var Tracer = otel.Tracer("compose")
// OTLPConfig contains the necessary values to initialize an OTLP client
// manually.
//
// This supports a minimal set of options based on what is necessary for
// automatic OTEL configuration from Docker context metadata.
type OTLPConfig struct {
Endpoint string
}
// ShutdownFunc flushes and stops an OTEL exporter.
type ShutdownFunc func(ctx context.Context) error
// envMap is a convenience type for OS environment variables.
type envMap map[string]string
func InitTracing(dockerCli command.Cli) (ShutdownFunc, error) {
// set global propagator to tracecontext (the default is no-op).
otel.SetTextMapPropagator(propagation.TraceContext{})
if v, _ := strconv.ParseBool(os.Getenv("COMPOSE_EXPERIMENTAL_OTEL")); !v {
return nil, nil
}
return InitProvider(dockerCli)
}
func InitProvider(dockerCli command.Cli) (ShutdownFunc, error) {
ctx := context.Background()
var errs []error
var exporters []sdktrace.SpanExporter
envClient, otelEnv := traceClientFromEnv()
if envClient != nil {
if envExporter, err := otlptrace.New(ctx, envClient); err != nil {
errs = append(errs, err)
} else if envExporter != nil {
exporters = append(exporters, envExporter)
}
}
if dcClient, err := traceClientFromDockerContext(dockerCli, otelEnv); err != nil {
errs = append(errs, err)
} else if dcClient != nil {
if dcExporter, err := otlptrace.New(ctx, dcClient); err != nil {
errs = append(errs, err)
} else if dcExporter != nil {
exporters = append(exporters, dcExporter)
}
}
if len(errs) != 0 {
return nil, errors.Join(errs...)
}
res, err := resource.New(
ctx,
resource.WithAttributes(
semconv.ServiceName("compose"),
semconv.ServiceVersion(internal.Version),
attribute.String("docker.context", dockerCli.CurrentContext()),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %v", err)
}
muxExporter := MuxExporter{exporters: exporters}
sp := sdktrace.NewSimpleSpanProcessor(muxExporter)
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(sp),
)
otel.SetTracerProvider(tracerProvider)
// Shutdown will flush any remaining spans and shut down the exporter.
return tracerProvider.Shutdown, nil
}
// traceClientFromEnv creates a GRPC OTLP client based on OS environment
// variables.
//
// https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
func traceClientFromEnv() (otlptrace.Client, envMap) {
hasOtelEndpointInEnv := false
otelEnv := make(map[string]string)
for _, kv := range os.Environ() {
k, v, ok := strings.Cut(kv, "=")
if !ok {
continue
}
if strings.HasPrefix(k, "OTEL_") {
otelEnv[k] = v
if strings.HasSuffix(k, "ENDPOINT") {
hasOtelEndpointInEnv = true
}
}
}
if !hasOtelEndpointInEnv {
return nil, nil
}
client := otlptracegrpc.NewClient()
return client, otelEnv
}

View File

@@ -0,0 +1,60 @@
/*
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 tracing_test
import (
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
"github.com/stretchr/testify/require"
"github.com/docker/compose/v2/internal/tracing"
)
var testStoreCfg = store.NewConfig(
func() interface{} {
return &map[string]interface{}{}
},
)
func TestExtractOtelFromContext(t *testing.T) {
if testing.Short() {
t.Skip("Requires filesystem access")
}
dir := t.TempDir()
st := store.New(dir, testStoreCfg)
err := st.CreateOrUpdate(store.Metadata{
Name: "test",
Metadata: command.DockerContext{
Description: t.Name(),
AdditionalFields: map[string]interface{}{
"otel": map[string]interface{}{
"OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:1234",
},
},
},
Endpoints: make(map[string]interface{}),
})
require.NoError(t, err)
cfg, err := tracing.ConfigFromDockerContext(st, "test")
require.NoError(t, err)
require.Equal(t, "localhost:1234", cfg.Endpoint)
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2020 Docker Compose CLI authors
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.

View File

@@ -74,6 +74,8 @@ type Service interface {
Events(ctx context.Context, projectName string, options EventsOptions) error
// Port executes the equivalent to a `compose port`
Port(ctx context.Context, projectName string, service string, port uint16, options PortOptions) (string, int, error)
// Publish executes the equivalent to a `compose publish`
Publish(ctx context.Context, project *types.Project, repository string) error
// Images executes the equivalent of a `compose images`
Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
// MaxConcurrency defines upper limit for concurrent operations against engine API
@@ -84,6 +86,15 @@ type Service interface {
Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
// Viz generates a graphviz graph of the project services
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
// Wait blocks until at least one of the services' container exits
Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error)
}
type WaitOptions struct {
// Services passed in the command line to be waited
Services []string
// Executes a down when a container exits
DownProjectOnContainerExit bool
}
type VizOptions struct {
@@ -121,6 +132,8 @@ type BuildOptions struct {
SSHs []types.SSHKey
// Memory limit for the build container
Memory int64
// Builder name passed in the command line
Builder string
}
// Apply mutates project according to build options
@@ -232,6 +245,8 @@ type DownOptions struct {
Images string
// Volumes remove volumes, both declared in the `volumes` section and anonymous ones
Volumes bool
// Services passed in the command line to be stopped
Services []string
}
// ConfigOptions group options of the Config API
@@ -247,6 +262,7 @@ type ConfigOptions struct {
// PushOptions group options of the Push API
type PushOptions struct {
Quiet bool
Repository string
IgnoreFailures bool
}
@@ -303,6 +319,8 @@ type RunOptions struct {
WorkingDir string
User string
Environment []string
CapAdd []string
CapDrop []string
Labels types.Labels
Privileged bool
UseNetworkAliases bool

View File

@@ -344,7 +344,7 @@ func (d *DryRunClient) ContainerCommit(ctx context.Context, container string, op
return d.apiClient.ContainerCommit(ctx, container, options)
}
func (d *DryRunClient) ContainerDiff(ctx context.Context, container string) ([]containerType.ContainerChangeResponseItem, error) {
func (d *DryRunClient) ContainerDiff(ctx context.Context, container string) ([]containerType.FilesystemChange, error) {
return d.apiClient.ContainerDiff(ctx, container)
}
@@ -616,7 +616,7 @@ func (d *DryRunClient) Info(ctx context.Context) (moby.Info, error) {
return d.apiClient.Info(ctx)
}
func (d *DryRunClient) RegistryLogin(ctx context.Context, auth moby.AuthConfig) (registry.AuthenticateOKBody, error) {
func (d *DryRunClient) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) {
return d.apiClient.RegistryLogin(ctx, auth)
}
@@ -636,8 +636,8 @@ func (d *DryRunClient) VolumeInspectWithRaw(ctx context.Context, volumeID string
return d.apiClient.VolumeInspectWithRaw(ctx, volumeID)
}
func (d *DryRunClient) VolumeList(ctx context.Context, filter filters.Args) (volume.ListResponse, error) {
return d.apiClient.VolumeList(ctx, filter)
func (d *DryRunClient) VolumeList(ctx context.Context, opts volume.ListOptions) (volume.ListResponse, error) {
return d.apiClient.VolumeList(ctx, opts)
}
func (d *DryRunClient) VolumesPrune(ctx context.Context, pruneFilter filters.Args) (moby.VolumesPruneReport, error) {

View File

@@ -54,6 +54,8 @@ type ServiceProxy struct {
MaxConcurrencyFn func(parallel int)
DryRunModeFn func(ctx context.Context, dryRun bool) (context.Context, error)
VizFn func(ctx context.Context, project *types.Project, options VizOptions) (string, error)
WaitFn func(ctx context.Context, projectName string, options WaitOptions) (int64, error)
PublishFn func(ctx context.Context, project *types.Project, repository string) error
interceptors []Interceptor
}
@@ -90,11 +92,13 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy {
s.TopFn = service.Top
s.EventsFn = service.Events
s.PortFn = service.Port
s.PublishFn = service.Publish
s.ImagesFn = service.Images
s.WatchFn = service.Watch
s.MaxConcurrencyFn = service.MaxConcurrency
s.DryRunModeFn = service.DryRunMode
s.VizFn = service.Viz
s.WaitFn = service.Wait
return s
}
@@ -309,6 +313,10 @@ func (s *ServiceProxy) Port(ctx context.Context, projectName string, service str
return s.PortFn(ctx, projectName, service, port, options)
}
func (s *ServiceProxy) Publish(ctx context.Context, project *types.Project, repository string) error {
return s.PublishFn(ctx, project, repository)
}
// Images implements Service interface
func (s *ServiceProxy) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) {
if s.ImagesFn == nil {
@@ -325,7 +333,7 @@ func (s *ServiceProxy) Watch(ctx context.Context, project *types.Project, servic
return s.WatchFn(ctx, project, services, options)
}
// Viz implements Viz interface
// Viz implements Service interface
func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error) {
if s.VizFn == nil {
return "", ErrNotImplemented
@@ -333,6 +341,14 @@ func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options
return s.VizFn(ctx, project, options)
}
// Wait implements Service interface
func (s *ServiceProxy) Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error) {
if s.WaitFn == nil {
return 0, ErrNotImplemented
}
return s.WaitFn(ctx, projectName, options)
}
func (s *ServiceProxy) MaxConcurrency(i int) {
s.MaxConcurrencyFn(i)
}

View File

@@ -22,6 +22,8 @@ import (
"os"
"path/filepath"
"github.com/docker/buildx/controller/pb"
"github.com/compose-spec/compose-go/types"
"github.com/containerd/containerd/platforms"
"github.com/docker/buildx/build"
@@ -38,6 +40,7 @@ import (
"github.com/moby/buildkit/util/entitlements"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
@@ -68,6 +71,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
// build and will lock
progressCtx, cancel := context.WithCancel(context.Background())
defer cancel()
w, err := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, options.Progress)
if err != nil {
return nil, err
@@ -112,11 +116,11 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
}
buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, flatten(args))
ids, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w)
digest, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w, options.Builder)
if err != nil {
return err
}
builtIDs[idx] = ids[service.Name]
builtIDs[idx] = digest
return nil
}, func(traversal *graphTraversal) {
@@ -175,20 +179,24 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
mode = xprogress.PrinterModeQuiet
}
err = s.prepareProjectForBuild(project, images)
if err != nil {
return err
}
builtImages, err := s.build(ctx, project, api.BuildOptions{
Progress: mode,
})
buildRequired, err := s.prepareProjectForBuild(project, images)
if err != nil {
return err
}
for name, digest := range builtImages {
images[name] = digest
if buildRequired {
builtImages, err := s.build(ctx, project, api.BuildOptions{
Progress: mode,
})
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
for i, service := range project.Services {
image := api.GetImageNameOrDefault(service, project.Name)
@@ -203,10 +211,11 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
return nil
}
func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) error {
func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) (bool, error) {
buildRequired := false
err := api.BuildOptions{}.Apply(project)
if err != nil {
return err
return false, err
}
for i, service := range project.Services {
if service.Build == nil {
@@ -227,8 +236,9 @@ func (s *composeService) prepareProjectForBuild(project *types.Project, images m
service.Build.Platforms = []string{service.Platform}
}
project.Services[i] = service
buildRequired = true
}
return nil
return buildRequired, nil
}
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
@@ -251,6 +261,9 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
for i, service := range project.Services {
imgName := api.GetImageNameOrDefault(service, project.Name)
digest, ok := images[imgName]
if !ok {
continue
}
if service.Platform != "" {
platform, err := platforms.Parse(service.Platform)
if err != nil {
@@ -271,9 +284,8 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
}
}
if ok {
project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
}
project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
}
return images, nil
@@ -356,8 +368,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
NamedContexts: toBuildContexts(service.Build.AdditionalContexts),
},
CacheFrom: cacheFrom,
CacheTo: cacheTo,
CacheFrom: pb.CreateCaches(cacheFrom),
CacheTo: pb.CreateCaches(cacheTo),
NoCache: service.Build.NoCache,
Pull: service.Build.Pull,
BuildArgs: buildArgs,
@@ -440,6 +452,9 @@ func addSecretsConfig(project *types.Project, service types.ServiceConfig) (sess
default:
return nil, fmt.Errorf("build.secrets only supports environment or file-based secrets: %q", secret.Source)
}
if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
logrus.Warn("secrets `uid`, `gid` and `mode` are not supported by BuildKit, they will be ignored")
}
}
store, err := secretsprovider.NewStore(sources)
if err != nil {

View File

@@ -20,44 +20,46 @@ import (
"context"
"crypto/sha1"
"fmt"
"path/filepath"
"github.com/docker/buildx/build"
"github.com/docker/buildx/builder"
_ "github.com/docker/buildx/driver/docker" //nolint:blank-imports
_ "github.com/docker/buildx/driver/docker-container" //nolint:blank-imports
_ "github.com/docker/buildx/driver/kubernetes" //nolint:blank-imports
_ "github.com/docker/buildx/driver/remote" //nolint:blank-imports
buildx "github.com/docker/buildx/util/progress"
"github.com/moby/buildkit/client"
"github.com/docker/buildx/build"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/dockerutil"
buildx "github.com/docker/buildx/util/progress"
"github.com/docker/compose/v2/pkg/progress"
"github.com/moby/buildkit/client"
)
func (s *composeService) doBuildBuildkit(ctx context.Context, service string, opts build.Options, p *buildx.Printer) (map[string]string, error) {
b, err := builder.New(s.dockerCli)
func (s *composeService) doBuildBuildkit(ctx context.Context, service string, opts build.Options, p *buildx.Printer, builderName string) (string, error) {
b, err := builder.New(s.dockerCli, builder.WithName(builderName))
if err != nil {
return nil, err
return "", err
}
nodes, err := b.LoadNodes(ctx, false)
if err != nil {
return nil, err
return "", err
}
var response map[string]*client.SolveResponse
if s.dryRun {
response = s.dryRunBuildResponse(ctx, service, opts)
} else {
response, err = build.Build(ctx, nodes, map[string]build.Options{service: opts}, dockerutil.NewClient(s.dockerCli), filepath.Dir(s.configFile().Filename), buildx.WithPrefix(p, service, true))
response, err = build.Build(ctx, nodes,
map[string]build.Options{service: opts},
dockerutil.NewClient(s.dockerCli),
confutil.ConfigDir(s.dockerCli),
buildx.WithPrefix(p, service, true))
if err != nil {
return nil, WrapCategorisedComposeError(err, BuildFailure)
return "", WrapCategorisedComposeError(err, BuildFailure)
}
}
imagesBuilt := map[string]string{}
for name, img := range response {
for _, img := range response {
if img == nil || len(img.ExporterResponse) == 0 {
continue
}
@@ -65,10 +67,10 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, service string, op
if !ok {
continue
}
imagesBuilt[name] = digest
return digest, nil
}
return imagesBuilt, err
return "", fmt.Errorf("buildkit response is missing expected result for %s", service)
}
func (s composeService) dryRunBuildResponse(ctx context.Context, name string, options build.Options) map[string]*client.SolveResponse {

View File

@@ -26,6 +26,8 @@ import (
"runtime"
"strings"
"github.com/docker/docker/api/types/registry"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command/image/build"
@@ -153,9 +155,9 @@ func (s *composeService) doBuildClassic(ctx context.Context, service types.Servi
if err != nil {
return "", err
}
authConfigs := make(map[string]dockertypes.AuthConfig, len(creds))
authConfigs := make(map[string]registry.AuthConfig, len(creds))
for k, auth := range creds {
authConfigs[k] = dockertypes.AuthConfig(auth)
authConfigs[k] = registry.AuthConfig(auth)
}
buildOptions := imageBuildOptions(service.Build)
buildOptions.Tags = append(buildOptions.Tags, service.Image)

View File

@@ -44,7 +44,7 @@ func TestPrepareProjectForBuild(t *testing.T) {
}
s := &composeService{}
err := s.prepareProjectForBuild(&project, nil)
_, err := s.prepareProjectForBuild(&project, nil)
assert.NilError(t, err)
assert.DeepEqual(t, project.Services[0].Build.Platforms, types.StringList{"alice/32"})
})
@@ -70,7 +70,7 @@ func TestPrepareProjectForBuild(t *testing.T) {
}
s := &composeService{}
err := s.prepareProjectForBuild(&project, nil)
_, err := s.prepareProjectForBuild(&project, nil)
assert.NilError(t, err)
assert.DeepEqual(t, project.Services[0].Build.Platforms, types.StringList{"linux/amd64"})
})
@@ -89,7 +89,7 @@ func TestPrepareProjectForBuild(t *testing.T) {
}
s := &composeService{}
err := s.prepareProjectForBuild(&project, map[string]string{"foo": "exists"})
_, err := s.prepareProjectForBuild(&project, map[string]string{"foo": "exists"})
assert.NilError(t, err)
assert.Check(t, project.Services[0].Build == nil)
})
@@ -115,7 +115,7 @@ func TestPrepareProjectForBuild(t *testing.T) {
}
s := &composeService{}
err := s.prepareProjectForBuild(&project, nil)
_, err := s.prepareProjectForBuild(&project, nil)
assert.Check(t, err != nil)
})
}

View File

@@ -26,6 +26,8 @@ import (
"strings"
"sync"
"github.com/docker/docker/api/types/volume"
"github.com/compose-spec/compose-go/types"
"github.com/distribution/distribution/v3/reference"
"github.com/docker/cli/cli/command"
@@ -230,7 +232,10 @@ SERVICES:
}
func (s *composeService) actualVolumes(ctx context.Context, projectName string) (types.Volumes, error) {
volumes, err := s.apiClient().VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
opts := volume.ListOptions{
Filters: filters.NewArgs(projectFilter(projectName)),
}
volumes, err := s.apiClient().VolumeList(ctx, opts)
if err != nil {
return nil, err
}
@@ -277,8 +282,11 @@ func (s *composeService) isSWarmEnabled(ctx context.Context) (bool, error) {
if err != nil {
swarmEnabled.err = err
}
if info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive {
swarmEnabled.val = info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive
switch info.Swarm.LocalNodeState {
case swarm.LocalNodeStateInactive, swarm.LocalNodeStateLocked:
swarmEnabled.val = false
default:
swarmEnabled.val = true
}
})
return swarmEnabled.val, swarmEnabled.err

View File

@@ -19,6 +19,7 @@ package compose
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"sync"
@@ -28,7 +29,6 @@ import (
"github.com/containerd/containerd/platforms"
moby "github.com/docker/docker/api/types"
containerType "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -172,6 +172,9 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
eg, _ := errgroup.WithContext(ctx)
sort.Slice(containers, func(i, j int) bool {
return containers[i].Created < containers[j].Created
})
for i, container := range containers {
if i >= expected {
// Scale Down
@@ -229,7 +232,13 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
name := getContainerName(project.Name, service, number)
i := i
eg.Go(func() error {
container, err := c.service.createContainer(ctx, project, service, name, number, false, true, false)
opts := createOptions{
AutoRemove: false,
AttachStdin: false,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels),
}
container, err := c.service.createContainer(ctx, project, service, name, number, opts)
updated[actual+i] = container
return err
})
@@ -395,12 +404,11 @@ func getScale(config types.ServiceConfig) (int, error) {
}
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
name string, number int, autoRemove bool, useNetworkAliases bool, attachStdin bool) (container moby.Container, err error) {
name string, number int, opts createOptions) (container moby.Container, err error) {
w := progress.ContextWriter(ctx)
eventName := "Container " + name
w.Event(progress.CreatingEvent(eventName))
container, err = s.createMobyContainer(ctx, project, service, name, number, nil,
autoRemove, useNetworkAliases, attachStdin, w, mergeLabels(service.Labels, service.CustomLabels))
container, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts, w)
if err != nil {
return
}
@@ -425,9 +433,13 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
}
name := getContainerName(project.Name, service, number)
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited,
false, true, false, w,
mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID))
opts := createOptions{
AutoRemove: false,
AttachStdin: false,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID),
}
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts, w)
if err != nil {
return created, err
}
@@ -486,13 +498,12 @@ func (s *composeService) createMobyContainer(ctx context.Context,
name string,
number int,
inherit *moby.Container,
autoRemove, useNetworkAliases, attachStdin bool,
opts createOptions,
w progress.Writer,
labels types.Labels,
) (moby.Container, error) {
var created moby.Container
containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, inherit,
autoRemove, attachStdin, labels)
cfgs, err := s.getCreateConfigs(ctx, project, service, number, inherit, opts)
if err != nil {
return created, err
}
@@ -509,7 +520,8 @@ func (s *composeService) createMobyContainer(ctx context.Context,
}
plat = &p
}
response, err := s.apiClient().ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
response, err := s.apiClient().ContainerCreate(ctx, cfgs.Container, cfgs.Host, cfgs.Network, plat, name)
if err != nil {
return created, err
}
@@ -532,33 +544,19 @@ func (s *composeService) createMobyContainer(ctx context.Context,
Networks: inspectedContainer.NetworkSettings.Networks,
},
}
links, err := s.getLinks(ctx, project.Name, service, number)
if err != nil {
return created, err
}
for _, netName := range service.NetworksByPriority() {
netwrk := project.Networks[netName]
cfg := service.Networks[netName]
aliases := []string{getContainerName(project.Name, service, number)}
if useNetworkAliases {
aliases = append(aliases, service.Name)
if cfg != nil {
aliases = append(aliases, cfg.Aliases...)
}
}
if val, ok := created.NetworkSettings.Networks[netwrk.Name]; ok {
if shortIDAliasExists(created.ID, val.Aliases...) {
continue
}
err = s.apiClient().NetworkDisconnect(ctx, netwrk.Name, created.ID, false)
if err != nil {
// the highest-priority network is the primary and is included in the ContainerCreate API
// call via container.NetworkMode & network.NetworkingConfig
// any remaining networks are connected one-by-one here after creation (but before start)
serviceNetworks := service.NetworksByPriority()
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
}
}
err = s.connectContainerToNetwork(ctx, created.ID, netwrk.Name, cfg, links, aliases...)
if err != nil {
return created, err
}
}
err = s.injectSecrets(ctx, project, service, created.ID)
@@ -623,43 +621,6 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
return links, nil
}
func shortIDAliasExists(containerID string, aliases ...string) bool {
for _, alias := range aliases {
if alias == containerID[:12] {
return true
}
}
return false
}
func (s *composeService) connectContainerToNetwork(ctx context.Context, id string, netwrk string, cfg *types.ServiceNetworkConfig, links []string, aliases ...string) error {
var (
ipv4Address string
ipv6Address string
ipam *network.EndpointIPAMConfig
)
if cfg != nil {
ipv4Address = cfg.Ipv4Address
ipv6Address = cfg.Ipv6Address
ipam = &network.EndpointIPAMConfig{
IPv4Address: ipv4Address,
IPv6Address: ipv6Address,
LinkLocalIPs: cfg.LinkLocalIPs,
}
}
err := s.apiClient().NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{
Aliases: aliases,
IPAddress: ipv4Address,
GlobalIPv6Address: ipv6Address,
Links: links,
IPAMConfig: ipam,
})
if err != nil {
return err
}
return nil
}
func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) {
for _, c := range containers {
container, err := s.apiClient().ContainerInspect(ctx, c.ID)

View File

@@ -325,5 +325,5 @@ func resolveLocalPath(localPath string) (absPath string, err error) {
if absPath, err = filepath.Abs(localPath); err != nil {
return
}
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
}

View File

@@ -48,6 +48,20 @@ import (
"github.com/docker/compose/v2/pkg/utils"
)
type createOptions struct {
AutoRemove bool
AttachStdin bool
UseNetworkAliases bool
Labels types.Labels
}
type createConfigs struct {
Container *container.Config
Host *container.HostConfig
Network *network.NetworkingConfig
Links []string
}
func (s *composeService) Create(ctx context.Context, project *types.Project, options api.CreateOptions) error {
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
return s.create(ctx, project, options)
@@ -106,11 +120,6 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
}
}
err = prepareServicesDependsOn(project)
if err != nil {
return err
}
return newConvergence(options.Services, observedState, s).apply(ctx, project, options)
}
@@ -147,78 +156,13 @@ func prepareNetworks(project *types.Project) {
}
}
func prepareServicesDependsOn(p *types.Project) error {
allServices := types.Project{}
allServices.Services = p.AllServices()
for i, service := range p.Services {
var dependencies []string
networkDependency := getDependentServiceFromMode(service.NetworkMode)
if networkDependency != "" {
dependencies = append(dependencies, networkDependency)
}
ipcDependency := getDependentServiceFromMode(service.Ipc)
if ipcDependency != "" {
dependencies = append(dependencies, ipcDependency)
}
pidDependency := getDependentServiceFromMode(service.Pid)
if pidDependency != "" {
dependencies = append(dependencies, pidDependency)
}
for _, vol := range service.VolumesFrom {
spec := strings.Split(vol, ":")
if len(spec) == 0 {
continue
}
if spec[0] == "container" {
continue
}
dependencies = append(dependencies, spec[0])
}
for _, link := range service.Links {
dependencies = append(dependencies, strings.Split(link, ":")[0])
}
for d := range service.DependsOn {
dependencies = append(dependencies, d)
}
if len(dependencies) == 0 {
continue
}
// Verify dependencies exist in the project, whether disabled or not
deps, err := allServices.GetServices(dependencies...)
if err != nil {
return err
}
if service.DependsOn == nil {
service.DependsOn = make(types.DependsOnConfig)
}
for _, d := range deps {
if _, ok := service.DependsOn[d.Name]; !ok {
service.DependsOn[d.Name] = types.ServiceDependency{
Condition: types.ServiceConditionStarted,
}
}
}
p.Services[i] = service
}
return nil
}
func (s *composeService) ensureNetworks(ctx context.Context, networks types.Networks) error {
for _, network := range networks {
err := s.ensureNetwork(ctx, network)
for i, network := range networks {
err := s.ensureNetwork(ctx, &network)
if err != nil {
return err
}
networks[i] = network
}
return nil
}
@@ -236,18 +180,16 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
return nil
}
func (s *composeService) getCreateOptions(ctx context.Context,
func (s *composeService) getCreateConfigs(ctx context.Context,
p *types.Project,
service types.ServiceConfig,
number int,
inherit *moby.Container,
autoRemove, attachStdin bool,
labels types.Labels,
) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
labels, err := s.prepareLabels(labels, service, number)
opts createOptions,
) (createConfigs, error) {
labels, err := s.prepareLabels(opts.Labels, service, number)
if err != nil {
return nil, nil, nil, err
return createConfigs{}, err
}
var (
@@ -266,11 +208,6 @@ func (s *composeService) getCreateOptions(ctx context.Context,
stdinOpen = service.StdinOpen
)
binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
if err != nil {
return nil, nil, nil, err
}
proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil))
env := proxyConfig.OverrideBy(service.Environment)
@@ -281,8 +218,8 @@ func (s *composeService) getCreateOptions(ctx context.Context,
ExposedPorts: buildContainerPorts(service),
Tty: tty,
OpenStdin: stdinOpen,
StdinOnce: attachStdin && stdinOpen,
AttachStdin: attachStdin,
StdinOnce: opts.AttachStdin && stdinOpen,
AttachStdin: opts.AttachStdin,
AttachStderr: true,
AttachStdout: true,
Cmd: runCmd,
@@ -298,20 +235,7 @@ func (s *composeService) getCreateOptions(ctx context.Context,
StopTimeout: ToSeconds(service.StopGracePeriod),
}
portBindings := buildContainerPortBindingOptions(service)
resources := getDeployResources(service)
if service.NetworkMode == "" {
service.NetworkMode = getDefaultNetworkMode(p, service)
}
var networkConfig *network.NetworkingConfig
for _, id := range service.NetworksByPriority() {
networkConfig = s.createNetworkConfig(p, service, id)
break
}
// VOLUMES/MOUNTS/FILESYSTEMS
tmpfs := map[string]string{}
for _, t := range service.Tmpfs {
if arr := strings.SplitN(t, ":", 2); len(arr) > 1 {
@@ -320,7 +244,28 @@ func (s *composeService) getCreateOptions(ctx context.Context,
tmpfs[arr[0]] = ""
}
}
binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
if err != nil {
return createConfigs{}, err
}
var volumesFrom []string
for _, v := range service.VolumesFrom {
if !strings.HasPrefix(v, "container:") {
return createConfigs{}, fmt.Errorf("invalid volume_from: %s", v)
}
volumesFrom = append(volumesFrom, v[len("container:"):])
}
// NETWORKING
links, err := s.getLinks(ctx, p.Name, service, number)
if err != nil {
return createConfigs{}, err
}
networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases)
portBindings := buildContainerPortBindingOptions(service)
// MISC
resources := getDeployResources(service)
var logConfig container.LogConfig
if service.Logging != nil {
logConfig = container.LogConfig{
@@ -328,31 +273,18 @@ func (s *composeService) getCreateOptions(ctx context.Context,
Config: service.Logging.Options,
}
}
var volumesFrom []string
for _, v := range service.VolumesFrom {
if !strings.HasPrefix(v, "container:") {
return nil, nil, nil, fmt.Errorf("invalid volume_from: %s", v)
}
volumesFrom = append(volumesFrom, v[len("container:"):])
}
links, err := s.getLinks(ctx, p.Name, service, number)
if err != nil {
return nil, nil, nil, err
}
securityOpts, unconfined, err := parseSecurityOpts(p, service.SecurityOpt)
if err != nil {
return nil, nil, nil, err
return createConfigs{}, err
}
hostConfig := container.HostConfig{
AutoRemove: autoRemove,
AutoRemove: opts.AutoRemove,
Binds: binds,
Mounts: mounts,
CapAdd: strslice.StrSlice(service.CapAdd),
CapDrop: strslice.StrSlice(service.CapDrop),
NetworkMode: container.NetworkMode(service.NetworkMode),
NetworkMode: networkMode,
Init: service.Init,
IpcMode: container.IpcMode(service.Ipc),
CgroupnsMode: container.CgroupnsMode(service.Cgroup),
@@ -387,12 +319,28 @@ func (s *composeService) getCreateOptions(ctx context.Context,
hostConfig.ReadonlyPaths = []string{}
}
return &containerConfig, &hostConfig, networkConfig, nil
cfgs := createConfigs{
Container: &containerConfig,
Host: &hostConfig,
Network: networkingConfig,
Links: links,
}
return cfgs, nil
}
func (s *composeService) createNetworkConfig(p *types.Project, service types.ServiceConfig, networkID string) *network.NetworkingConfig {
net := p.Networks[networkID]
config := service.Networks[networkID]
func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, useNetworkAliases bool) []string {
aliases := []string{getContainerName(project.Name, service, serviceIndex)}
if useNetworkAliases {
aliases = append(aliases, service.Name)
if cfg := service.Networks[networkKey]; cfg != nil {
aliases = append(aliases, cfg.Aliases...)
}
}
return aliases
}
func createEndpointSettings(p *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, links []string, useNetworkAliases bool) *network.EndpointSettings {
config := service.Networks[networkKey]
var ipam *network.EndpointIPAMConfig
var (
ipv4Address string
@@ -407,15 +355,12 @@ func (s *composeService) createNetworkConfig(p *types.Project, service types.Ser
LinkLocalIPs: config.LinkLocalIPs,
}
}
return &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
net.Name: {
Aliases: getAliases(service, config),
IPAddress: ipv4Address,
IPv6Gateway: ipv6Address,
IPAMConfig: ipam,
},
},
return &network.EndpointSettings{
Aliases: getAliases(p, service, serviceIndex, networkKey, useNetworkAliases),
Links: links,
IPAddress: ipv4Address,
IPv6Gateway: ipv6Address,
IPAMConfig: ipam,
}
}
@@ -474,17 +419,39 @@ func (s *composeService) prepareLabels(labels types.Labels, service types.Servic
return labels, nil
}
func getDefaultNetworkMode(project *types.Project, service types.ServiceConfig) string {
// defaultNetworkSettings determines the container.NetworkMode and corresponding network.NetworkingConfig (nil if not applicable).
func defaultNetworkSettings(
project *types.Project,
service types.ServiceConfig,
serviceIndex int,
links []string,
useNetworkAliases bool,
) (container.NetworkMode, *network.NetworkingConfig) {
if service.NetworkMode != "" {
return container.NetworkMode(service.NetworkMode), nil
}
if len(project.Networks) == 0 {
return "none"
return "none", nil
}
var networkKey string
if len(service.Networks) > 0 {
name := service.NetworksByPriority()[0]
return project.Networks[name].Name
networkKey = service.NetworksByPriority()[0]
} else {
networkKey = "default"
}
return project.Networks["default"].Name
mobyNetworkName := project.Networks[networkKey].Name
epSettings := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases)
networkConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
mobyNetworkName: epSettings,
},
}
// From the Engine API docs:
// > Supported standard values are: bridge, host, none, and container:<name|id>.
// > Any other value is taken as a custom network's name to which this container should connect to.
return container.NetworkMode(mobyNetworkName), networkConfig
}
func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
@@ -640,8 +607,8 @@ func setLimits(limits *types.Resource, resources *container.Resources) {
resources.NanoCPUs = int64(f * 1e9)
}
}
if limits.PIds > 0 {
resources.PidsLimit = &limits.PIds
if limits.Pids > 0 {
resources.PidsLimit = &limits.Pids
}
}
@@ -743,7 +710,10 @@ func getVolumesFrom(project *types.Project, volumesFrom []string) ([]string, []s
}
func getDependentServiceFromMode(mode string) string {
if strings.HasPrefix(mode, types.NetworkModeServicePrefix) {
if strings.HasPrefix(
mode,
types.NetworkModeServicePrefix,
) {
return mode[len(types.NetworkModeServicePrefix):]
}
return ""
@@ -1069,15 +1039,133 @@ func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
}
}
func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
aliases := []string{s.Name}
if c != nil {
aliases = append(aliases, c.Aliases...)
func (s *composeService) ensureNetwork(ctx context.Context, n *types.NetworkConfig) error {
if n.External.External {
return s.resolveExternalNetwork(ctx, n)
}
return aliases
err := s.resolveOrCreateNetwork(ctx, n)
if errdefs.IsConflict(err) {
// Maybe another execution of `docker compose up|run` created same network
// let's retry once
return s.resolveOrCreateNetwork(ctx, n)
}
return err
}
func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
func (s *composeService) resolveOrCreateNetwork(ctx context.Context, n *types.NetworkConfig) error { //nolint:gocyclo
expectedNetworkLabel := n.Labels[api.NetworkLabel]
expectedProjectLabel := n.Labels[api.ProjectLabel]
// First, try to find a unique network matching by name or ID
inspect, err := s.apiClient().NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
if err == nil {
// NetworkInspect will match on ID prefix, so double check we get the expected one
// as looking for network named `db` we could erroneously matched network ID `db9086999caf`
if inspect.Name == n.Name || inspect.ID == n.Name {
p, ok := inspect.Labels[api.ProjectLabel]
if !ok {
logrus.Warnf("a network with name %s exists but was not created by compose.\n"+
"Set `external: true` to use an existing network", n.Name)
} else if p != expectedProjectLabel {
logrus.Warnf("a network with name %s exists but was not created for project %q.\n"+
"Set `external: true` to use an existing network", n.Name, expectedProjectLabel)
}
if inspect.Labels[api.NetworkLabel] != expectedNetworkLabel {
return fmt.Errorf("network %s was found but has incorrect label %s set to %q", n.Name, api.NetworkLabel, inspect.Labels[api.NetworkLabel])
}
return nil
}
}
// ignore other errors. Typically, an ambiguous request by name results in some generic `invalidParameter` error
// Either not found, or name is ambiguous - use NetworkList to list by name
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("name", n.Name)),
})
if err != nil {
return err
}
// NetworkList Matches all or part of a network name, so we have to filter for a strict match
networks = utils.Filter(networks, func(net moby.NetworkResource) bool {
return net.Name == n.Name
})
for _, net := range networks {
if net.Labels[api.ProjectLabel] == expectedProjectLabel &&
net.Labels[api.NetworkLabel] == expectedNetworkLabel {
return nil
}
}
// we could have set NetworkList with a projectFilter and networkFilter but not doing so allows to catch this
// scenario were a network with same name exists but doesn't have label, and use of `CheckDuplicate: true`
// prevents to create another one.
if len(networks) > 0 {
logrus.Warnf("a network with name %s exists but was not created by compose.\n"+
"Set `external: true` to use an existing network", n.Name)
return nil
}
var ipam *network.IPAM
if n.Ipam.Config != nil {
var config []network.IPAMConfig
for _, pool := range n.Ipam.Config {
config = append(config, network.IPAMConfig{
Subnet: pool.Subnet,
IPRange: pool.IPRange,
Gateway: pool.Gateway,
AuxAddress: pool.AuxiliaryAddresses,
})
}
ipam = &network.IPAM{
Driver: n.Ipam.Driver,
Config: config,
}
}
createOpts := moby.NetworkCreate{
CheckDuplicate: true,
Labels: n.Labels,
Driver: n.Driver,
Options: n.DriverOpts,
Internal: n.Internal,
Attachable: n.Attachable,
IPAM: ipam,
EnableIPv6: n.EnableIPv6,
}
if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
createOpts.IPAM = &network.IPAM{}
}
if n.Ipam.Driver != "" {
createOpts.IPAM.Driver = n.Ipam.Driver
}
for _, ipamConfig := range n.Ipam.Config {
config := network.IPAMConfig{
Subnet: ipamConfig.Subnet,
IPRange: ipamConfig.IPRange,
Gateway: ipamConfig.Gateway,
AuxAddress: ipamConfig.AuxiliaryAddresses,
}
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
}
networkEventName := fmt.Sprintf("Network %s", n.Name)
w := progress.ContextWriter(ctx)
w.Event(progress.CreatingEvent(networkEventName))
_, err = s.apiClient().NetworkCreate(ctx, n.Name, createOpts)
if err != nil {
w.Event(progress.ErrorEvent(networkEventName))
return errors.Wrapf(err, "failed to create network %s", n.Name)
}
w.Event(progress.CreatedEvent(networkEventName))
return nil
}
func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.NetworkConfig) error {
// NetworkInspect will match on ID prefix, so NetworkList with a name
// filter is used to look for an exact match to prevent e.g. a network
// named `db` from getting erroneously matched to a network with an ID
@@ -1088,87 +1176,35 @@ func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfi
if err != nil {
return err
}
networkNotFound := true
for _, net := range networks {
if net.Name == n.Name {
networkNotFound = false
break
}
}
if networkNotFound {
if n.External.External {
if n.Driver == "overlay" {
// Swarm nodes do not register overlay networks that were
// created on a different node unless they're in use.
// Here we assume `driver` is relevant for a network we don't manage
// which is a non-sense, but this is our legacy ¯\(ツ)/¯
// networkAttach will later fail anyway if network actually doesn't exists
enabled, err := s.isSWarmEnabled(ctx)
if err != nil {
return err
}
if enabled {
return nil
}
}
return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
}
var ipam *network.IPAM
if n.Ipam.Config != nil {
var config []network.IPAMConfig
for _, pool := range n.Ipam.Config {
config = append(config, network.IPAMConfig{
Subnet: pool.Subnet,
IPRange: pool.IPRange,
Gateway: pool.Gateway,
AuxAddress: pool.AuxiliaryAddresses,
})
}
ipam = &network.IPAM{
Driver: n.Ipam.Driver,
Config: config,
}
}
createOpts := moby.NetworkCreate{
CheckDuplicate: true,
// TODO NameSpace Labels
Labels: n.Labels,
Driver: n.Driver,
Options: n.DriverOpts,
Internal: n.Internal,
Attachable: n.Attachable,
IPAM: ipam,
EnableIPv6: n.EnableIPv6,
}
if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 {
createOpts.IPAM = &network.IPAM{}
}
// NetworkList API doesn't return the exact name match, so we can retrieve more than one network with a request
networks = utils.Filter(networks, func(net moby.NetworkResource) bool {
return net.Name == n.Name
})
if n.Ipam.Driver != "" {
createOpts.IPAM.Driver = n.Ipam.Driver
}
for _, ipamConfig := range n.Ipam.Config {
config := network.IPAMConfig{
Subnet: ipamConfig.Subnet,
IPRange: ipamConfig.IPRange,
Gateway: ipamConfig.Gateway,
AuxAddress: ipamConfig.AuxiliaryAddresses,
}
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
}
networkEventName := fmt.Sprintf("Network %s", n.Name)
w := progress.ContextWriter(ctx)
w.Event(progress.CreatingEvent(networkEventName))
if _, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts); err != nil {
w.Event(progress.ErrorEvent(networkEventName))
return errors.Wrapf(err, "failed to create network %s", n.Name)
}
w.Event(progress.CreatedEvent(networkEventName))
switch len(networks) {
case 1:
n.Name = networks[0].ID
return nil
case 0:
if n.Driver == "overlay" {
// Swarm nodes do not register overlay networks that were
// created on a different node unless they're in use.
// Here we assume `driver` is relevant for a network we don't manage
// which is a non-sense, but this is our legacy ¯\(ツ)/¯
// networkAttach will later fail anyway if network actually doesn't exists
enabled, err := s.isSWarmEnabled(ctx)
if err != nil {
return err
}
if enabled {
return nil
}
}
return fmt.Errorf("network %s declared as external, but could not be found", n.Name)
default:
return fmt.Errorf("multiple networks with name %q were found. Use network ID as `name` to avoid ambiguity", n.Name)
}
return nil
}
func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) error {
@@ -1194,7 +1230,7 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo
logrus.Warnf("volume %q already exists but was not created by Docker Compose. Use `external: true` to use an existing volume", volume.Name)
}
if ok && p != project {
logrus.Warnf("volume %q already exists but was not created for project %q. Use `external: true` to use an existing volume", volume.Name, p)
logrus.Warnf("volume %q already exists but was created for project %q (expected %q). Use `external: true` to use an existing volume", volume.Name, p, project)
}
return nil
}

View File

@@ -22,6 +22,8 @@ import (
"sort"
"testing"
"gotest.tools/v3/assert/cmp"
"github.com/docker/compose/v2/pkg/api"
composetypes "github.com/compose-spec/compose-go/types"
@@ -203,7 +205,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
assert.Equal(t, mounts[2].Target, "\\\\.\\pipe\\docker_engine")
}
func TestGetDefaultNetworkMode(t *testing.T) {
func TestDefaultNetworkSettings(t *testing.T) {
t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
@@ -231,7 +233,10 @@ func TestGetDefaultNetworkMode(t *testing.T) {
}),
}
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_myNetwork2")
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
assert.Equal(t, string(networkMode), "myProject_myNetwork2")
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork2"))
})
t.Run("returns default network when service has no networks", func(t *testing.T) {
@@ -256,7 +261,10 @@ func TestGetDefaultNetworkMode(t *testing.T) {
}),
}
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_default")
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
assert.Equal(t, string(networkMode), "myProject_default")
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_default"))
})
t.Run("returns none if project has no networks", func(t *testing.T) {
@@ -270,6 +278,28 @@ func TestGetDefaultNetworkMode(t *testing.T) {
},
}
assert.Equal(t, getDefaultNetworkMode(&project, service), "none")
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
assert.Equal(t, string(networkMode), "none")
assert.Check(t, cmp.Nil(networkConfig))
})
t.Run("returns defined network mode if explicitly set", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
NetworkMode: "host",
}
project := composetypes.Project{
Name: "myProject",
Services: []composetypes.ServiceConfig{service},
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
"default": {
Name: "myProject_default",
},
}),
}
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
assert.Equal(t, string(networkMode), "host")
assert.Check(t, cmp.Nil(networkConfig))
})
}

View File

@@ -23,6 +23,8 @@ import (
"sync"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/utils"
@@ -38,8 +40,9 @@ const (
)
type graphTraversal struct {
mu sync.Mutex
seen map[string]struct{}
mu sync.Mutex
seen map[string]struct{}
ignored map[string]struct{}
extremityNodesFn func(*Graph) []*Vertex // leaves or roots
adjacentNodesFn func(*Vertex) []*Vertex // getParents or getChildren
@@ -75,7 +78,7 @@ func downDirectionTraversal(visitorFn func(context.Context, string) error) *grap
// InDependencyOrder applies the function to the services of the project taking in account the dependency order
func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error {
graph, err := NewGraph(project.Services, ServiceStopped)
graph, err := NewGraph(project, ServiceStopped)
if err != nil {
return err
}
@@ -87,15 +90,46 @@ func InDependencyOrder(ctx context.Context, project *types.Project, fn func(cont
}
// InReverseDependencyOrder applies the function to the services of the project in reverse order of dependencies
func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error {
graph, err := NewGraph(project.Services, ServiceStarted)
func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversal)) error {
graph, err := NewGraph(project, ServiceStarted)
if err != nil {
return err
}
t := downDirectionTraversal(fn)
for _, option := range options {
option(t)
}
return t.visit(ctx, graph)
}
func WithRootNodesAndDown(nodes []string) func(*graphTraversal) {
return func(t *graphTraversal) {
if len(nodes) == 0 {
return
}
originalFn := t.extremityNodesFn
t.extremityNodesFn = func(graph *Graph) []*Vertex {
var want []string
for _, node := range nodes {
vertex := graph.Vertices[node]
want = append(want, vertex.Service)
for _, v := range getAncestors(vertex) {
want = append(want, v.Service)
}
}
t.ignored = map[string]struct{}{}
for k := range graph.Vertices {
if !utils.Contains(want, k) {
t.ignored[k] = struct{}{}
}
}
return originalFn(graph)
}
}
}
func (t *graphTraversal) visit(ctx context.Context, g *Graph) error {
expect := len(g.Vertices)
if expect == 0 {
@@ -106,24 +140,28 @@ func (t *graphTraversal) visit(ctx context.Context, g *Graph) error {
if t.maxConcurrency > 0 {
eg.SetLimit(t.maxConcurrency + 1)
}
nodeCh := make(chan *Vertex)
nodeCh := make(chan *Vertex, expect)
defer close(nodeCh)
// nodeCh need to allow n=expect writers while reader goroutine could have returner after ctx.Done
eg.Go(func() error {
for node := range nodeCh {
expect--
if expect == 0 {
close(nodeCh)
for {
select {
case <-ctx.Done():
return nil
case node := <-nodeCh:
expect--
if expect == 0 {
return nil
}
t.run(ctx, g, eg, t.adjacentNodesFn(node), nodeCh)
}
t.run(ctx, g, eg, t.adjacentNodesFn(node), nodeCh)
}
return nil
})
nodes := t.extremityNodesFn(g)
t.run(ctx, g, eg, nodes, nodeCh)
err := eg.Wait()
return err
return eg.Wait()
}
// Note: this could be `graph.walk` or whatever
@@ -142,7 +180,10 @@ func (t *graphTraversal) run(ctx context.Context, graph *Graph, eg *errgroup.Gro
}
eg.Go(func() error {
err := t.visitorFn(ctx, node.Service)
var err error
if _, ignore := t.ignored[node.Service]; !ignore {
err = t.visitorFn(ctx, node.Service)
}
if err == nil {
graph.UpdateStatus(node.Key, t.targetServiceStatus)
}
@@ -197,6 +238,16 @@ func getChildren(v *Vertex) []*Vertex {
return v.GetChildren()
}
// getAncestors return all descendents for a vertex, might contain duplicates
func getAncestors(v *Vertex) []*Vertex {
var descendents []*Vertex
for _, parent := range v.GetParents() {
descendents = append(descendents, parent)
descendents = append(descendents, getAncestors(parent)...)
}
return descendents
}
// GetChildren returns a slice with the child vertices of the a Vertex
func (v *Vertex) GetChildren() []*Vertex {
var res []*Vertex
@@ -207,19 +258,28 @@ func (v *Vertex) GetChildren() []*Vertex {
}
// NewGraph returns the dependency graph of the services
func NewGraph(services types.Services, initialStatus ServiceStatus) (*Graph, error) {
func NewGraph(project *types.Project, initialStatus ServiceStatus) (*Graph, error) {
graph := &Graph{
lock: sync.RWMutex{},
Vertices: map[string]*Vertex{},
}
for _, s := range services {
for _, s := range project.Services {
graph.AddVertex(s.Name, s.Name, initialStatus)
}
for _, s := range services {
for _, s := range project.Services {
for _, name := range s.GetDependencies() {
_ = graph.AddEdge(s.Name, name)
err := graph.AddEdge(s.Name, name)
if err != nil {
if api.IsNotFoundError(err) {
ds, err := project.GetDisabledService(name)
if err == nil {
return nil, fmt.Errorf("service %s is required by %s but is disabled. Can be enabled by profiles %s", name, s.Name, ds.Profiles)
}
}
return nil, err
}
}
}
@@ -259,10 +319,10 @@ func (g *Graph) AddEdge(source string, destination string) error {
destinationVertex := g.Vertices[destination]
if sourceVertex == nil {
return fmt.Errorf("could not find %s", source)
return errors.Wrapf(api.ErrNotFound, "could not find %s", source)
}
if destinationVertex == nil {
return fmt.Errorf("could not find %s", destination)
return errors.Wrapf(api.ErrNotFound, "could not find %s", destination)
}
// If they are already connected

View File

@@ -19,9 +19,12 @@ package compose
import (
"context"
"fmt"
"sort"
"sync"
"testing"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/utils"
testify "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
@@ -267,7 +270,7 @@ func TestBuildGraph(t *testing.T) {
Services: tC.services,
}
graph, err := NewGraph(project.Services, ServiceStopped)
graph, err := NewGraph(&project, ServiceStopped)
assert.NilError(t, err, fmt.Sprintf("failed to build graph for: %s", tC.desc))
for k, vertex := range graph.Vertices {
@@ -297,3 +300,88 @@ func isVertexEqual(a, b Vertex) bool {
childrenEquality &&
parentEquality
}
func TestWith_RootNodesAndUp(t *testing.T) {
graph := &Graph{
lock: sync.RWMutex{},
Vertices: map[string]*Vertex{},
}
/** graph topology:
A B
/ \ / \
G C E
\ /
D
|
F
*/
graph.AddVertex("A", "A", 0)
graph.AddVertex("B", "B", 0)
graph.AddVertex("C", "C", 0)
graph.AddVertex("D", "D", 0)
graph.AddVertex("E", "E", 0)
graph.AddVertex("F", "F", 0)
graph.AddVertex("G", "G", 0)
_ = graph.AddEdge("C", "A")
_ = graph.AddEdge("C", "B")
_ = graph.AddEdge("E", "B")
_ = graph.AddEdge("D", "C")
_ = graph.AddEdge("D", "E")
_ = graph.AddEdge("F", "D")
_ = graph.AddEdge("G", "A")
tests := []struct {
name string
nodes []string
want []string
}{
{
name: "whole graph",
nodes: []string{"A", "B"},
want: []string{"A", "B", "C", "D", "E", "F", "G"},
},
{
name: "only leaves",
nodes: []string{"F", "G"},
want: []string{"F", "G"},
},
{
name: "simple dependent",
nodes: []string{"D"},
want: []string{"D", "F"},
},
{
name: "diamond dependents",
nodes: []string{"B"},
want: []string{"B", "C", "D", "E", "F"},
},
{
name: "partial graph",
nodes: []string{"A"},
want: []string{"A", "C", "D", "F", "G"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mx := sync.Mutex{}
expected := utils.Set[string]{}
expected.AddAll("C", "G", "D", "F")
var visited []string
gt := downDirectionTraversal(func(ctx context.Context, s string) error {
mx.Lock()
defer mx.Unlock()
visited = append(visited, s)
return nil
})
WithRootNodesAndDown(tt.nodes)(gt)
err := gt.visit(context.TODO(), graph)
assert.NilError(t, err)
sort.Strings(visited)
assert.DeepEqual(t, tt.want, visited)
})
}
}

View File

@@ -44,7 +44,7 @@ func (s *composeService) Down(ctx context.Context, projectName string, options a
}, s.stdinfo())
}
func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error {
func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error { //nolint:gocyclo
w := progress.ContextWriter(ctx)
resourceToRemove := false
@@ -65,6 +65,12 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
}
}
// Check requested services exists in model
options.Services, err = checkSelectedServices(options, project)
if err != nil {
return err
}
if len(containers) > 0 {
resourceToRemove = true
}
@@ -73,7 +79,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
serviceContainers := containers.filter(isService(service))
err := s.removeContainers(ctx, w, serviceContainers, options.Timeout, options.Volumes)
return err
})
}, WithRootNodesAndDown(options.Services))
if err != nil {
return err
}
@@ -111,6 +117,23 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
return eg.Wait()
}
func checkSelectedServices(options api.DownOptions, project *types.Project) ([]string, error) {
var services []string
for _, service := range options.Services {
_, err := project.GetService(service)
if err != nil {
if options.Project != nil {
// ran with an explicit compose.yaml file, so we should not ignore
return nil, err
}
// ran without an explicit compose.yaml file, so can't distinguish typo vs container already removed
} else {
services = append(services, service)
}
}
return services, nil
}
func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
var ops []downOp
for _, vol := range project.Volumes {
@@ -148,32 +171,30 @@ func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Pr
func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
var ops []downOp
for _, n := range project.Networks {
for key, n := range project.Networks {
if n.External.External {
continue
}
// loop capture variable for op closure
networkName := n.Name
networkKey := key
idOrName := n.Name
ops = append(ops, func() error {
return s.removeNetwork(ctx, networkName, w)
return s.removeNetwork(ctx, networkKey, project.Name, idOrName, w)
})
}
return ops
}
func (s *composeService) removeNetwork(ctx context.Context, name string, w progress.Writer) error {
// networks are guaranteed to have unique IDs but NOT names, so it's
// possible to get into a situation where a compose down will fail with
// an error along the lines of:
// failed to remove network test: Error response from daemon: network test is ambiguous (2 matches found based on name)
// as a workaround here, the delete is done by ID after doing a list using
// the name as a filter (99.9% of the time this will return a single result)
func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName string, projectName string, name string, w progress.Writer) error {
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("name", name)),
Filters: filters.NewArgs(
projectFilter(projectName),
networkFilter(composeNetworkName)),
})
if err != nil {
return errors.Wrapf(err, fmt.Sprintf("failed to inspect network %s", name))
return errors.Wrapf(err, "failed to list networks")
}
if len(networks) == 0 {
return nil
}
@@ -229,6 +250,10 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre
w.Event(progress.NewEvent(id, progress.Done, "Removed"))
return nil
}
if errdefs.IsConflict(err) {
w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
return nil
}
if errdefs.IsNotFound(err) {
w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
return nil
@@ -244,6 +269,10 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
return nil
}
if errdefs.IsConflict(err) {
w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
return nil
}
if errdefs.IsNotFound(err) {
w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
return nil

View File

@@ -53,15 +53,19 @@ func TestDown(t *testing.T) {
testContainer("service2", "789", false),
testContainer("service_orphan", "321", true),
}, nil)
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{}, nil)
// network names are not guaranteed to be unique, ensure Compose handles
// cleanup properly if duplicates are inadvertently created
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]moby.NetworkResource{
{ID: "abc123", Name: "myProject_default"},
{ID: "def456", Name: "myProject_default"},
{ID: "abc123", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
{ID: "def456", Name: "myProject_default", Labels: map[string]string{compose.NetworkLabel: "default"}},
}, nil)
stopOptions := containerType.StopOptions{}
@@ -74,7 +78,9 @@ func TestDown(t *testing.T) {
api.EXPECT().ContainerRemove(gomock.Any(), "789", moby.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("name", "myProject_default")),
Filters: filters.NewArgs(
projectFilter(strings.ToLower(testProject)),
networkFilter("default")),
}).Return([]moby.NetworkResource{
{ID: "abc123", Name: "myProject_default"},
{ID: "def456", Name: "myProject_default"},
@@ -103,10 +109,18 @@ func TestDownRemoveOrphans(t *testing.T) {
testContainer("service2", "789", false),
testContainer("service_orphan", "321", true),
}, nil)
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{}, nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]moby.NetworkResource{{Name: "myProject_default"}}, nil)
Return([]moby.NetworkResource{
{
Name: "myProject_default",
Labels: map[string]string{compose.NetworkLabel: "default"},
}}, nil)
stopOptions := containerType.StopOptions{}
api.EXPECT().ContainerStop(gomock.Any(), "123", stopOptions).Return(nil)
@@ -118,7 +132,10 @@ func TestDownRemoveOrphans(t *testing.T) {
api.EXPECT().ContainerRemove(gomock.Any(), "321", moby.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("name", "myProject_default")),
Filters: filters.NewArgs(
networkFilter("default"),
projectFilter(strings.ToLower(testProject)),
),
}).Return([]moby.NetworkResource{{ID: "abc123", Name: "myProject_default"}}, nil)
api.EXPECT().NetworkInspect(gomock.Any(), "abc123", gomock.Any()).Return(moby.NetworkResource{ID: "abc123"}, nil)
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
@@ -138,7 +155,11 @@ func TestDownRemoveVolumes(t *testing.T) {
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
[]moby.Container{testContainer("service1", "123", false)}, nil)
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{
Volumes: []*volume.Volume{{Name: "myProject_volume"}},
}, nil)
@@ -271,7 +292,11 @@ func TestDownRemoveImages_NoLabel(t *testing.T) {
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return(
[]moby.Container{container}, nil)
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{
Volumes: []*volume.Volume{{Name: "myProject_volume"}},
}, nil)

View File

@@ -25,7 +25,6 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
)
@@ -67,8 +66,8 @@ func (s *composeService) Events(ctx context.Context, projectName string, options
err := options.Consumer(api.Event{
Timestamp: timestamp,
Service: service,
Container: event.ID,
Status: event.Status,
Container: event.Actor.ID,
Status: event.Action,
Attributes: attributes,
})
if err != nil {

View File

@@ -31,6 +31,10 @@ func serviceFilter(serviceName string) filters.KeyValuePair {
return filters.Arg("label", fmt.Sprintf("%s=%s", api.ServiceLabel, serviceName))
}
func networkFilter(name string) filters.KeyValuePair {
return filters.Arg("label", fmt.Sprintf("%s=%s", api.NetworkLabel, name))
}
func oneOffFilter(b bool) filters.KeyValuePair {
v := "False"
if b {

View File

@@ -28,11 +28,12 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
// remove the Build config when generating the service hash
o.Build = nil
o.PullPolicy = ""
o.Scale = 1
if o.Deploy != nil {
var one uint64 = 1
o.Deploy.Replicas = &one
if o.Deploy == nil {
o.Deploy = &types.DeployConfig{}
}
o.Scale = 1
var one uint64 = 1
o.Deploy.Replicas = &one
bytes, err := json.Marshal(o)
if err != nil {
return "", err

View File

@@ -50,7 +50,11 @@ func TestKillAll(t *testing.T) {
Filters: filters.NewArgs(projectFilter(name), hasConfigHashLabel()),
}).Return(
[]moby.Container{testContainer("service1", "123", false), testContainer("service1", "456", false), testContainer("service2", "789", false)}, nil)
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{}, nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]moby.NetworkResource{
@@ -81,7 +85,11 @@ func TestKillSignal(t *testing.T) {
ctx := context.Background()
api.EXPECT().ContainerList(ctx, listOptions).Return([]moby.Container{testContainer(serviceName, "123", false)}, nil)
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{}, nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]moby.NetworkResource{

View File

@@ -45,19 +45,12 @@ func (s *composeService) Logs(
return err
}
project := options.Project
if project == nil {
project, err = s.getProjectWithResources(ctx, containers, projectName)
if err != nil {
return err
}
if options.Project != nil && len(options.Services) == 0 {
// we run with an explicit compose.yaml, so only consider services defined in this file
options.Services = options.Project.ServiceNames()
containers = containers.filter(isService(options.Services...))
}
if len(options.Services) == 0 {
options.Services = project.ServiceNames()
}
containers = containers.filter(isService(options.Services...))
eg, ctx := errgroup.WithContext(ctx)
for _, c := range containers {
c := c

View File

@@ -71,9 +71,9 @@ func TestComposeService_Logs_Demux(t *testing.T) {
c1Stdout := stdcopy.NewStdWriter(c1Writer, stdcopy.Stdout)
c1Stderr := stdcopy.NewStdWriter(c1Writer, stdcopy.Stderr)
go func() {
_, err := c1Stdout.Write([]byte("hello stdout\n"))
_, err := c1Stdout.Write([]byte("hello\n stdout"))
assert.NoError(t, err, "Writing to fake stdout")
_, err = c1Stderr.Write([]byte("hello stderr\n"))
_, err = c1Stderr.Write([]byte("hello\n stderr"))
assert.NoError(t, err, "Writing to fake stderr")
_ = c1Writer.Close()
}()
@@ -94,7 +94,7 @@ func TestComposeService_Logs_Demux(t *testing.T) {
require.Equal(
t,
[]string{"hello stdout", "hello stderr"},
[]string{"hello", " stdout", "hello", " stderr"},
consumer.LogsForContainer("c"),
)
}

185
pkg/compose/publish.go Normal file
View File

@@ -0,0 +1,185 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/compose-spec/compose-go/types"
"github.com/distribution/distribution/v3/reference"
client2 "github.com/docker/cli/cli/registry/client"
"github.com/docker/compose/v2/pkg/api"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string) error {
err := s.Push(ctx, project, api.PushOptions{})
if err != nil {
return err
}
target, err := reference.ParseDockerRef(repository)
if err != nil {
return err
}
client := s.dockerCli.RegistryClient(false)
for i, service := range project.Services {
ref, err := reference.ParseDockerRef(service.Image)
if err != nil {
return err
}
auth, err := encodedAuth(ref, s.configFile())
if err != nil {
return err
}
inspect, err := s.apiClient().DistributionInspect(ctx, ref.String(), auth)
if err != nil {
return err
}
canonical, err := reference.WithDigest(ref, inspect.Descriptor.Digest)
if err != nil {
return err
}
to, err := reference.WithDigest(target, inspect.Descriptor.Digest)
if err != nil {
return err
}
err = client.MountBlob(ctx, canonical, to)
switch err.(type) {
case client2.ErrBlobCreated:
default:
return err
}
service.Image = to.String()
project.Services[i] = service
}
err = s.publishComposeYaml(ctx, project, repository)
if err != nil {
return err
}
return nil
}
func (s *composeService) publishComposeYaml(ctx context.Context, project *types.Project, repository string) error {
ref, err := reference.ParseDockerRef(repository)
if err != nil {
return err
}
var manifests []v1.Descriptor
for _, composeFile := range project.ComposeFiles {
stat, err := os.Stat(composeFile)
if err != nil {
return err
}
cmd := exec.CommandContext(ctx, "oras", "push", "--artifact-type", "application/vnd.docker.compose.yaml", ref.String(), composeFile)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = s.stderr()
err = cmd.Start()
if err != nil {
return err
}
out, err := io.ReadAll(stdout)
if err != nil {
return err
}
var composeFileDigest string
for _, line := range strings.Split(string(out), "\n") {
if strings.HasPrefix(line, "Digest: ") {
composeFileDigest = line[len("Digest: "):]
}
fmt.Fprintln(s.stdout(), line)
}
if composeFileDigest == "" {
return fmt.Errorf("expected oras to display `Digest: xxx`")
}
err = cmd.Wait()
if err != nil {
return err
}
manifests = append(manifests, v1.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: digest.Digest(composeFileDigest),
Size: stat.Size(),
ArtifactType: "application/vnd.docker.compose.yaml",
})
}
for _, service := range project.Services {
dockerRef, err := reference.ParseDockerRef(service.Image)
if err != nil {
return err
}
manifests = append(manifests, v1.Descriptor{
MediaType: v1.MediaTypeImageIndex,
Digest: dockerRef.(reference.Digested).Digest(),
Annotations: map[string]string{
"com.docker.compose.service": service.Name,
},
})
}
manifest := v1.Index{
Versioned: specs.Versioned{
SchemaVersion: 2,
},
MediaType: v1.MediaTypeImageIndex,
Manifests: manifests,
Annotations: map[string]string{
"com.docker.compose": api.ComposeVersion,
},
}
manifestContent, err := json.Marshal(manifest)
if err != nil {
return err
}
temp, err := os.CreateTemp(os.TempDir(), "compose")
if err != nil {
return err
}
err = os.WriteFile(temp.Name(), manifestContent, 0o700)
if err != nil {
return err
}
defer os.Remove(temp.Name())
cmd := exec.CommandContext(ctx, "oras", "manifest", "push", ref.String(), temp.Name())
cmd.Stdout = s.stdout()
cmd.Stderr = s.stderr()
err = cmd.Run()
if err != nil {
return err
}
return nil
}

View File

@@ -17,6 +17,7 @@
package compose
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
@@ -26,14 +27,17 @@ import (
"github.com/compose-spec/compose-go/types"
"github.com/distribution/distribution/v3/reference"
"github.com/docker/buildx/driver"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/registry/remote"
)
func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
@@ -45,8 +49,8 @@ func (s *composeService) Push(ctx context.Context, project *types.Project, optio
}, s.stdinfo(), "Pushing")
}
func (s *composeService) push(ctx context.Context, project *types.Project, options api.PushOptions) error {
eg, ctx := errgroup.WithContext(ctx)
func (s *composeService) push(upctx context.Context, project *types.Project, options api.PushOptions) error {
eg, ctx := errgroup.WithContext(upctx)
eg.SetLimit(s.maxConcurrency)
info, err := s.apiClient().Info(ctx)
@@ -79,7 +83,66 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
return nil
})
}
return eg.Wait()
err = eg.Wait()
if err != nil {
return err
}
ctx = upctx
if options.Repository != "" {
repository, err := remote.NewRepository(options.Repository)
if err != nil {
return err
}
yaml, err := project.MarshalYAML()
if err != nil {
return err
}
manifests := []v1.Descriptor{
{
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Digest: digest.FromBytes(yaml),
Size: int64(len(yaml)),
Data: yaml,
ArtifactType: "application/vnd.docker.compose.yaml",
},
}
for _, service := range project.Services {
inspected, _, err := s.dockerCli.Client().ImageInspectWithRaw(ctx, service.Image)
if err != nil {
return err
}
manifests = append(manifests, v1.Descriptor{
MediaType: v1.MediaTypeImageIndex,
Digest: digest.Digest(inspected.RepoDigests[0]),
Size: inspected.Size,
Annotations: map[string]string{
"com.docker.compose.service": service.Name,
},
})
}
manifest := v1.Index{
MediaType: v1.MediaTypeImageIndex,
Manifests: manifests,
Annotations: map[string]string{
"com.docker.compose": api.ComposeVersion,
},
}
manifestContent, err := json.Marshal(manifest)
if err != nil {
return err
}
manifestDescriptor := content.NewDescriptorFromBytes(v1.MediaTypeImageIndex, manifestContent)
err = repository.Push(ctx, manifestDescriptor, bytes.NewReader(manifestContent))
if err != nil {
return err
}
}
return nil
}
func (s *composeService) pushServiceImage(ctx context.Context, service types.ServiceConfig, info moby.Info, configFile driver.Auth, w progress.Writer, quietPush bool) error {

View File

@@ -19,11 +19,14 @@ package compose
import (
"context"
"fmt"
"os"
"os/signal"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli"
cmd "github.com/docker/cli/cli/command/container"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/pkg/stringid"
)
@@ -38,6 +41,14 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
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()
sigc := make(chan os.Signal, 128)
signal.Notify(sigc)
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
@@ -88,8 +99,14 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
return "", err
}
}
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1,
opts.AutoRemove, opts.UseNetworkAliases, opts.Interactive)
createOpts := createOptions{
AutoRemove: opts.AutoRemove,
AttachStdin: opts.Interactive,
UseNetworkAliases: opts.UseNetworkAliases,
Labels: mergeLabels(service.Labels, service.CustomLabels),
}
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, createOpts)
if err != nil {
return "", err
}
@@ -107,6 +124,15 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
if len(opts.User) > 0 {
service.User = opts.User
}
if len(opts.CapAdd) > 0 {
service.CapAdd = append(service.CapAdd, opts.CapAdd...)
service.CapDrop = utils.Remove(service.CapDrop, opts.CapAdd...)
}
if len(opts.CapDrop) > 0 {
service.CapDrop = append(service.CapDrop, opts.CapDrop...)
service.CapAdd = utils.Remove(service.CapAdd, opts.CapDrop...)
}
if len(opts.WorkingDir) > 0 {
service.WorkingDir = opts.WorkingDir
}

View File

@@ -45,7 +45,7 @@ func (s *composeService) injectSecrets(ctx context.Context, project *types.Proje
}
err = s.apiClient().CopyToContainer(ctx, id, "/", &b, moby.CopyToContainerOptions{
CopyUIDGID: true,
CopyUIDGID: config.UID != "" || config.GID != "",
})
if err != nil {
return err

View File

@@ -155,13 +155,31 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
required = services
}
// predicate to tell if a container we receive event for should be considered or ignored
ofInterest := func(c moby.Container) bool {
if len(services) > 0 {
// we only watch some services
return utils.Contains(services, c.Labels[api.ServiceLabel])
}
return true
}
// predicate to tell if a container we receive event for should be watched until termination
isRequired := func(c moby.Container) bool {
if len(services) > 0 && len(required) > 0 {
// we only watch some services
return utils.Contains(required, c.Labels[api.ServiceLabel])
}
return true
}
var (
expected []string
watched = map[string]int{}
replaced []string
)
for _, c := range containers {
if utils.Contains(required, c.Labels[api.ServiceLabel]) {
if isRequired(c) {
expected = append(expected, c.ID)
}
watched[c.ID] = 0
@@ -265,6 +283,11 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
if utils.Contains(expected, id) {
expected = append(expected, container.ID)
}
} else if ofInterest(container) {
watched[container.ID] = 1
if isRequired(container) {
expected = append(expected, container.ID)
}
}
}
if len(expected) == 0 {

View File

@@ -50,7 +50,11 @@ func TestStopTimeout(t *testing.T) {
testContainer("service1", "456", false),
testContainer("service2", "789", false),
}, nil)
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
api.EXPECT().VolumeList(
gomock.Any(),
volume.ListOptions{
Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject))),
}).
Return(volume.ListResponse{}, nil)
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).
Return([]moby.NetworkResource{}, nil)

67
pkg/compose/wait.go Normal file
View File

@@ -0,0 +1,67 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"github.com/docker/compose/v2/pkg/api"
"golang.org/x/sync/errgroup"
)
func (s *composeService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
containers, err := s.getContainers(ctx, projectName, oneOffInclude, false, options.Services...)
if err != nil {
return 0, err
}
if len(containers) == 0 {
return 0, fmt.Errorf("no containers for project %q", projectName)
}
eg, waitCtx := errgroup.WithContext(ctx)
var statusCode int64
for _, c := range containers {
c := c
eg.Go(func() error {
var err error
resultC, errC := s.dockerCli.Client().ContainerWait(waitCtx, c.ID, "")
select {
case result := <-resultC:
fmt.Fprintf(s.dockerCli.Out(), "container %q exited with status code %d\n", c.ID, result.StatusCode)
statusCode = result.StatusCode
case err = <-errC:
}
return err
})
}
err = eg.Wait()
if err != nil {
return 42, err // Ignore abort flag in case of error in wait
}
if options.DownProjectOnContainerExit {
return statusCode, s.Down(ctx, projectName, api.DownOptions{
RemoveOrphans: true,
})
}
return statusCode, err
}

View File

@@ -79,7 +79,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
needRebuild := make(chan fileMapping)
needSync := make(chan fileMapping)
err := s.prepareProjectForBuild(project, nil)
_, err := s.prepareProjectForBuild(project, nil)
if err != nil {
return err
}
@@ -104,7 +104,11 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
return err
}
if config != nil && len(config.Watch) > 0 && service.Build == nil {
if config == nil {
continue
}
if len(config.Watch) > 0 && service.Build == nil {
// service configured with watchers but no build section
return fmt.Errorf("can't watch service %q without a build context", service.Name)
}
@@ -118,21 +122,8 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
continue
}
if config == nil {
config = &DevelopmentConfig{
Watch: []Trigger{
{
Path: service.Build.Context,
Action: WatchActionRebuild,
},
},
}
}
name := service.Name
bc := service.Build.Context
dockerIgnores, err := watch.LoadDockerIgnore(bc)
dockerIgnores, err := watch.LoadDockerIgnore(service.Build.Context)
if err != nil {
return err
}
@@ -150,12 +141,21 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
dotGitIgnore,
)
watcher, err := watch.NewWatcher([]string{bc}, ignore)
var paths []string
for _, trigger := range config.Watch {
if checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path)
continue
}
paths = append(paths, trigger.Path)
}
watcher, err := watch.NewWatcher(paths, ignore)
if err != nil {
return err
}
fmt.Fprintf(s.stdinfo(), "watching %s\n", bc)
fmt.Fprintf(s.stdinfo(), "watching %s\n", paths)
err = watcher.Start()
if err != nil {
return err
@@ -311,25 +311,50 @@ func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project,
case <-ctx.Done():
return nil
case opt := <-needSync:
if fi, statErr := os.Stat(opt.HostPath); statErr == nil && !fi.IsDir() {
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
service, err := project.GetService(opt.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)
}
fmt.Fprintf(s.stdinfo(), "%s updated\n", opt.ContainerPath)
} else if errors.Is(statErr, fs.ErrNotExist) {
_, err := s.Exec(ctx, project.Name, api.RunOptions{
Service: opt.Service,
Command: []string{"rm", "-rf", opt.ContainerPath},
Index: 1,
})
if err != nil {
logrus.Warnf("failed to delete %q from %s: %v", opt.ContainerPath, opt.Service, err)
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 container\n", opt.ContainerPath)
fmt.Fprintf(s.stdinfo(), "%s deleted from service\n", opt.ContainerPath)
}
}
}
@@ -362,3 +387,12 @@ func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, i
}
}
}
func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
for _, volume := range volumes {
if volume.Bind != nil && strings.HasPrefix(watchPath, volume.Source) {
return true
}
}
return false
}

View File

@@ -17,6 +17,7 @@
package e2e
import (
"fmt"
"net/http"
"runtime"
"strings"
@@ -386,7 +387,7 @@ func TestBuildPlatformsStandardErrors(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
res.Assert(t, icmd.Expected{
ExitCode: 17,
Err: `multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")`,
Err: `Multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")`,
})
})
@@ -423,3 +424,30 @@ func TestBuildPlatformsStandardErrors(t *testing.T) {
})
}
func TestBuildBuilder(t *testing.T) {
c := NewParallelCLI(t)
builderName := "build-with-builder"
// declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", builderName, "--use", "--bootstrap")
assert.NilError(t, result.Error)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", builderName)
})
t.Run("use specific builder to run build command", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--builder", builderName)
assert.NilError(t, res.Error, res.Stderr())
})
t.Run("error when using specific builder to run build command", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test", "build", "--builder", "unknown-builder")
res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: fmt.Sprintf(`no builder %q found`, "unknown-builder"),
})
})
}

View File

@@ -102,6 +102,14 @@ func TestLocalComposeUp(t *testing.T) {
res.Assert(t, icmd.Expected{Out: `compose-e2e-demo-words-1 gtardif/sentences-api latest`})
})
t.Run("down SERVICE", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "web")
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "ps")
assert.Assert(t, !strings.Contains(res.Combined(), "compose-e2e-demo-web-1"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "compose-e2e-demo-db-1"), res.Combined())
})
t.Run("down", func(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
@@ -269,7 +277,7 @@ networks:
})
}
func TestStopWithDependeciesAttached(t *testing.T) {
func TestStopWithDependenciesAttached(t *testing.T) {
const projectName = "compose-e2e-stop-with-deps"
c := NewParallelCLI(t, WithEnv("COMMAND=echo hello"))

View File

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

View File

@@ -0,0 +1,11 @@
services:
faster:
image: alpine
command: sleep 2
slower:
image: alpine
command: sleep 5
infinity:
image: alpine
command: sleep infinity

View File

@@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
@@ -134,7 +135,7 @@ func initializePlugins(t testing.TB, configDir string) {
require.NoError(t, os.MkdirAll(filepath.Join(configDir, "cli-plugins"), 0o755),
"Failed to create cli-plugins directory")
composePlugin, err := findExecutable(DockerComposeExecutableName)
if os.IsNotExist(err) {
if errors.Is(err, fs.ErrNotExist) {
t.Logf("WARNING: docker-compose cli-plugin not found")
}
@@ -161,20 +162,21 @@ func dirContents(dir string) []string {
}
func findExecutable(executableName string) (string, error) {
_, filename, _, _ := runtime.Caller(0)
root := filepath.Join(filepath.Dir(filename), "..", "..")
buildPath := filepath.Join(root, "bin", "build")
bin, err := filepath.Abs(filepath.Join(buildPath, executableName))
if err != nil {
return "", err
bin := os.Getenv("COMPOSE_E2E_BIN_PATH")
if bin == "" {
_, filename, _, _ := runtime.Caller(0)
buildPath := filepath.Join(filepath.Dir(filename), "..", "..", "bin", "build")
var err error
bin, err = filepath.Abs(filepath.Join(buildPath, executableName))
if err != nil {
return "", err
}
}
if _, err := os.Stat(bin); err == nil {
return bin, nil
}
return "", errors.Wrap(os.ErrNotExist, "executable not found")
return "", fmt.Errorf("looking for %q: %w", bin, fs.ErrNotExist)
}
func findPluginExecutable(pluginExecutableName string) (string, error) {

View File

@@ -17,10 +17,13 @@
package e2e
import (
"fmt"
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/poll"
"gotest.tools/v3/icmd"
)
@@ -56,3 +59,42 @@ func TestLocalComposeLogs(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
}
func TestLocalComposeLogsFollow(t *testing.T) {
c := NewCLI(t, WithEnv("REPEAT=20"))
const projectName = "compose-e2e-logs"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d", "ping")
cmd := c.NewDockerComposeCmd(t, "--project-name", projectName, "logs", "-f")
res := icmd.StartCmd(cmd)
t.Cleanup(func() {
_ = res.Cmd.Process.Kill()
})
expected := fmt.Sprintf("%s-ping-1 ", projectName)
poll.WaitOn(t, expectOutput(res, expected), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d")
expected = fmt.Sprintf("%s-hello-1 ", projectName)
poll.WaitOn(t, expectOutput(res, expected), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
c.RunDockerComposeCmd(t, "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "up", "-d", "--scale", "ping=2", "ping")
expected = fmt.Sprintf("%s-ping-2 ", projectName)
poll.WaitOn(t, expectOutput(res, expected), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(20*time.Second))
}
func expectOutput(res *icmd.Result, expected string) func(t poll.LogT) poll.Result {
return func(t poll.LogT) poll.Result {
if strings.Contains(res.Stdout(), expected) {
return poll.Success()
}
return poll.Continue("condition not met")
}
}

View File

@@ -23,13 +23,14 @@ import (
"context"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"github.com/docker/compose/v2/pkg/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
@@ -79,7 +80,7 @@ func TestUpDependenciesNotStopped(t *testing.T) {
defer cancel()
cmd, err := StartWithNewGroupID(ctx, testCmd, upOut, nil)
assert.NoError(t, err, "Failed to run compose up")
assert.NilError(t, err, "Failed to run compose up")
t.Log("Waiting for containers to be in running state")
upOut.RequireEventuallyContains(t, "hello app")
@@ -138,3 +139,17 @@ func TestUpWithDependencyExit(t *testing.T) {
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "dependency failed to start: container dependencies-db-1 exited (1)"})
})
}
func TestScaleDoesntRecreate(t *testing.T) {
c := NewCLI(t)
const projectName = "compose-e2e-scale"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml", "--project-name", projectName, "up", "-d")
res := c.RunDockerComposeCmd(t, "-f", "fixtures/simple-composefile/compose.yaml", "--project-name", projectName, "up", "--scale", "simple=2", "-d")
assert.Check(t, !strings.Contains(res.Combined(), "Recreated"))
}

72
pkg/e2e/wait_test.go Normal file
View File

@@ -0,0 +1,72 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"strings"
"testing"
"time"
"gotest.tools/v3/assert"
)
func TestWaitOnFaster(t *testing.T) {
const projectName = "e2e-wait-faster"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "faster")
}
func TestWaitOnSlower(t *testing.T) {
const projectName = "e2e-wait-slower"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "slower")
}
func TestWaitOnInfinity(t *testing.T) {
const projectName = "e2e-wait-infinity"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
finished := make(chan struct{})
ticker := time.NewTicker(7 * time.Second)
go func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "infinity")
finished <- struct{}{}
}()
select {
case <-finished:
t.Fatal("wait infinity should not finish")
case <-ticker.C:
}
}
func TestWaitAndDrop(t *testing.T) {
const projectName = "e2e-wait-and-drop"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "--down-project", "faster")
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
}

View File

@@ -267,10 +267,10 @@ func (mr *MockAPIClientMockRecorder) ContainerCreate(arg0, arg1, arg2, arg3, arg
}
// ContainerDiff mocks base method.
func (m *MockAPIClient) ContainerDiff(arg0 context.Context, arg1 string) ([]container.ContainerChangeResponseItem, error) {
func (m *MockAPIClient) ContainerDiff(arg0 context.Context, arg1 string) ([]container.FilesystemChange, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ContainerDiff", arg0, arg1)
ret0, _ := ret[0].([]container.ContainerChangeResponseItem)
ret0, _ := ret[0].([]container.FilesystemChange)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -1381,7 +1381,7 @@ func (mr *MockAPIClientMockRecorder) PluginUpgrade(arg0, arg1, arg2 interface{})
}
// RegistryLogin mocks base method.
func (m *MockAPIClient) RegistryLogin(arg0 context.Context, arg1 types.AuthConfig) (registry.AuthenticateOKBody, error) {
func (m *MockAPIClient) RegistryLogin(arg0 context.Context, arg1 registry.AuthConfig) (registry.AuthenticateOKBody, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RegistryLogin", arg0, arg1)
ret0, _ := ret[0].(registry.AuthenticateOKBody)
@@ -1768,7 +1768,7 @@ func (mr *MockAPIClientMockRecorder) VolumeInspectWithRaw(arg0, arg1 interface{}
}
// VolumeList mocks base method.
func (m *MockAPIClient) VolumeList(arg0 context.Context, arg1 filters.Args) (volume.ListResponse, error) {
func (m *MockAPIClient) VolumeList(arg0 context.Context, arg1 volume.ListOptions) (volume.ListResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VolumeList", arg0, arg1)
ret0, _ := ret[0].(volume.ListResponse)

View File

@@ -423,6 +423,21 @@ func (mr *MockServiceMockRecorder) Viz(ctx, project, options interface{}) *gomoc
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Viz", reflect.TypeOf((*MockService)(nil).Viz), ctx, project, options)
}
// Wait mocks base method.
func (m *MockService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Wait", ctx, projectName, options)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Wait indicates an expected call of Wait.
func (mr *MockServiceMockRecorder) Wait(ctx, projectName, options interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockService)(nil).Wait), ctx, projectName, options)
}
// Watch mocks base method.
func (m *MockService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
m.ctrl.T.Helper()

View File

@@ -153,6 +153,15 @@ func RemovedEvent(id string) Event {
return NewEvent(id, Done, "Removed")
}
// SkippedEvent creates a new Skipped Event
func SkippedEvent(id string, reason string) Event {
return Event{
ID: id,
Status: Warning,
StatusText: "Skipped: " + reason,
}
}
// NewEvent new event
func NewEvent(id string, status EventStatus, statusText string) Event {
return Event{

37
pkg/progress/quiet.go Normal file
View File

@@ -0,0 +1,37 @@
/*
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 progress
import "context"
type quiet struct{}
func (q quiet) Start(_ context.Context) error {
return nil
}
func (q quiet) Stop() {
}
func (q quiet) Event(_ Event) {
}
func (q quiet) Events(_ []Event) {
}
func (q quiet) TailMsgf(_ string, _ ...interface{}) {
}

View File

@@ -21,12 +21,12 @@ import (
"io"
"sync"
"github.com/cloudflare/cfssl/log"
"github.com/docker/compose/v2/pkg/api"
"github.com/containerd/console"
"github.com/moby/term"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api"
)
// Writer can write multiple progress events
@@ -107,6 +107,8 @@ const (
ModeTTY = "tty"
// ModePlain dump raw events to output
ModePlain = "plain"
// ModeQuiet don't display events
ModeQuiet = "quiet"
)
// Mode define how progress should be rendered, either as ModePlain or ModeTTY
@@ -119,13 +121,16 @@ func NewWriter(ctx context.Context, out io.Writer, progressTitle string) (Writer
if !ok {
dryRun = false
}
if Mode == ModeQuiet {
return quiet{}, nil
}
f, isConsole := out.(console.File) // see https://github.com/docker/compose/issues/10560
if Mode == ModeAuto && isTerminal && isConsole {
return newTTYWriter(f, dryRun, progressTitle)
}
if Mode == ModeTTY {
if !isConsole {
log.Warning("Terminal is not a POSIX console")
logrus.Warn("Terminal is not a POSIX console")
} else {
return newTTYWriter(f, dryRun, progressTitle)
}

View File

@@ -20,8 +20,18 @@ func (s Set[T]) Add(v T) {
s[v] = struct{}{}
}
func (s Set[T]) Remove(v T) {
delete(s, v)
func (s Set[T]) AddAll(v ...T) {
for _, e := range v {
s[e] = struct{}{}
}
}
func (s Set[T]) Remove(v T) bool {
_, ok := s[v]
if ok {
delete(s, v)
}
return ok
}
func (s Set[T]) Clear() {

View File

@@ -39,3 +39,13 @@ func Remove[T any](origin []T, elements ...T) []T {
}
return filtered
}
func Filter[T any](elements []T, predicate func(T) bool) []T {
var filtered []T
for _, v := range elements {
if predicate(v) {
filtered = append(filtered, v)
}
}
return filtered
}

View File

@@ -43,11 +43,17 @@ func (s *splitWriter) Write(b []byte) (int, error) {
for {
b = s.buffer.Bytes()
index := bytes.Index(b, []byte{'\n'})
if index < 0 {
if index > 0 {
line := s.buffer.Next(index + 1)
s.consumer(string(line[:len(line)-1]))
} else {
line := s.buffer.String()
s.buffer.Reset()
if len(line) > 0 {
s.consumer(line)
}
break
}
line := s.buffer.Next(index + 1)
s.consumer(string(line[:len(line)-1]))
}
return n, nil
}

View File

@@ -28,13 +28,21 @@ func TestSplitWriter(t *testing.T) {
w := GetWriter(func(line string) {
lines = append(lines, line)
})
w.Write([]byte("h"))
w.Write([]byte("e"))
w.Write([]byte("l"))
w.Write([]byte("l"))
w.Write([]byte("o"))
w.Write([]byte("\n"))
w.Write([]byte("world!\n"))
w.Write([]byte("hello\n"))
w.Write([]byte("world\n"))
w.Write([]byte("!"))
assert.DeepEqual(t, lines, []string{"hello", "world", "!"})
}
//nolint:errcheck
func TestSplitWriterNoEOL(t *testing.T) {
var lines []string
w := GetWriter(func(line string) {
lines = append(lines, line)
})
w.Write([]byte("hello\n"))
w.Write([]byte("world!"))
assert.DeepEqual(t, lines, []string{"hello", "world!"})
}

View File

@@ -68,7 +68,7 @@ type Notify interface {
// - Watch /src/repo, but ignore /src/repo/.git
// - Watch /src/repo, but ignore everything in /src/repo/bazel-bin except /src/repo/bazel-bin/app-binary
//
// The PathMatcher inteface helps us manage these ignores.
// The PathMatcher interface helps us manage these ignores.
type PathMatcher interface {
Matches(file string) (bool, error)

View File

@@ -59,7 +59,7 @@ func dedupePathsForRecursiveWatcher(paths []string) []string {
}
if IsChild(current, existing) {
// Mark the element empty fo removal.
// Mark the element empty for removal.
result[i] = ""
hasRemovals = true
}