Compare commits

...

107 Commits

Author SHA1 Message Date
Milas Bowman
d6f842b042 test: e2e test reliability improvements (#10950)
* Use unique project name prefixes (some of these tests assert
  on output using the project name as a magic string, so could
  be impacted by other tests with the same project name prefix)
* Tear down port range project before starting to try and avoid
  race conditions with the engine and port assignment

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 16:01:15 -04:00
Milas Bowman
4fbbf201cd build(deps): upgrade to compose-go v1.18.3 (#10947)
https://github.com/compose-spec/compose-go/releases/tag/v1.18.3

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 09:17:57 -04:00
Bilal Khan
935d72f46f added the dot at the end of the sentence
Signed-off-by: Bilal Khan <bilalkhanrecovered@gmail.com>
2023-08-28 09:19:26 +02:00
Nicolas De Loof
41682acc77 add support for attributes exposed by docker ps
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-25 16:36:45 +02:00
Nicolas De Loof
1054792b47 align docker compose ps with docker CLI to support --format
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-25 16:36:45 +02:00
Milas Bowman
19f66918cc watch: only allow a single instance per-project
This is a good place to start introducing (local) exclusivity
to Compose. Now, when `alpha watch` launches, it will check for
the existence of a PID file in the user XDG runtime directory,
and create one if the existing one is stale or does not exist.
If the PID file exists and is valid, an error is returned and
Compose exits.

A slight tweak to the experimental remote Git loader has been
made to use the XDG package for consistency.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-25 15:49:28 +02:00
Milas Bowman
186744e034 ci: bump golangci-lint to v1.54.2
Also improve incremental lint caching.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-24 08:57:47 +02:00
Milas Bowman
bc9d696fa0 Merge pull request #10922 from thaJeztah/replace_dockerignore
replace dockerfile/dockerignore with patternmatcher/ignorefile
2023-08-23 16:04:19 -04:00
Nicolas De loof
6204fb1c94 logs: fix for missing output on container exit (#10925)
We can't assume we receive container logs line by line. Some framework won't buffer output and will send char by char, and we also can receive looong lines which get buffered to 32kb and then cut into multiple logs.

This assumes we will catch container streams being closed before we receive a die event for container, which could be subject to race condition, but at least the impact here is minimal and the fix works for reproduction examples provided in linked issues.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-23 08:57:18 -04:00
Sebastiaan van Stijn
5d732010a7 replace dockerfile/dockerignore with patternmatcher/ignorefile
The BuildKit dockerignore package was integrated in the patternmatcher
repository / module. This patch updates our uses of the BuildKit package
with its new location.

A small local change was made to keep the format of the existing error message,
because the "ignorefile" package is slightly more agnostic in that respect
and doesn't include ".dockerignore" in the error message.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-08-23 00:43:17 +02:00
Sebastiaan van Stijn
2006f3fe7d go.mod: github.com/moby/patternmatcher v0.6.0
- integrate frontend/dockerfile/dockerignore from buildkit

full diff: https://github.com/moby/patternmatcher/compare/v0.5.0...v0.6.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-08-23 00:40:12 +02:00
Sebastiaan van Stijn
192718c001 go.mod: remove some outdated comments
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-08-23 00:39:35 +02:00
Milas Bowman
c79f67fead otel: add include to project up span
Flatten the list of included files and add as a slice attribute.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-22 16:10:18 +02:00
dependabot[bot]
3b294bfdda build(deps): bump github.com/compose-spec/compose-go from 1.18.1 to 1.18.2 (#10915)
build(deps): bump github.com/compose-spec/compose-go

Bumps [github.com/compose-spec/compose-go](https://github.com/compose-spec/compose-go) from 1.18.1 to 1.18.2.
- [Release notes](https://github.com/compose-spec/compose-go/releases)
- [Commits](https://github.com/compose-spec/compose-go/compare/v1.18.1...v1.18.2)

---
updated-dependencies:
- dependency-name: github.com/compose-spec/compose-go
  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-08-21 14:02:03 -04:00
Nicolas De loof
dd34f7a22b include: add experimental support for Git resources (#10811)
Requires setting `COMPOSE_EXPERIMENTAL_GIT_REMOTE=1`.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-18 09:16:45 -04:00
Milas Bowman
caad72713b up: handle various attach use cases better
By default, `compose up` attaches to all services (i.e.
shows log output from every associated container). If
a service is specified, e.g. `compose up foo`, then
only `foo`'s logs are tailed. The `--attach-dependencies`
flag can also be used, so that if `foo` depended upon
`bar`, then `bar`'s logs would also be followed. It's
also possible to use `--no-attach` to filter out one
or more services explicitly, e.g. `compose up --no-attach=noisy`
would launch all services, including `noisy`, and would
show log output from every service _except_ `noisy`.
Lastly, it's possible to use `up --attach` to explicitly
restrict to a subset of services (or their dependencies).

How these flags interact with each other is also worth
thinking through.

There were a few different connected issues here, but
the primary issue was that running `compose up foo` was
always attaching dependencies regardless of `--attach-dependencies`.

The filtering logic here has been updated so that it
behaves predictably both when launching all services
(`compose up`) or a subset (`compose up foo`) as well
as various flag combinations on top of those.

Notably, this required making some changes to how it
watches containers. The logic here between attaching
for logs and monitoring for lifecycle changes is
tightly coupled, so some changes were needed to ensure
that the full set of services being `up`'d are _watched_
and the subset that should have logs shown are _attached_.
(This does mean faking the attach with an event but not
actually doing it.)

While handling that, I adjusted the context lifetimes
here, which improves error handling that gets shown to
the user and should help avoid potential leaks by getting
rid of a `context.Background()`.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-18 12:38:38 +02:00
Nicolas De loof
792afb8d13 build: use correct values for proxy variables (#10908)
clone variable before we capture a pointer

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-17 14:25:28 -04:00
Nicolas De Loof
150449bbd2 warn user secret uid/gid/mode is not supported
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-16 19:17:28 +02:00
Guillaume Lours
8d0df18762 Merge pull request #10867 from docker/dependabot/go_modules/github.com/moby/buildkit-0.12.1
build(deps): bump github.com/moby/buildkit from 0.12.1-0.20230717122532-faa0cc7da353 to 0.12.1
2023-08-11 10:45:05 +02:00
dependabot[bot]
5b53f8e47f build(deps): bump github.com/moby/buildkit
Bumps [github.com/moby/buildkit](https://github.com/moby/buildkit) from 0.12.1-0.20230717122532-faa0cc7da353 to 0.12.1.
- [Release notes](https://github.com/moby/buildkit/releases)
- [Commits](https://github.com/moby/buildkit/commits/v0.12.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-11 08:28:23 +00:00
Guillaume Lours
c5fef61383 Merge pull request #10893 from glours/bump-compose-go-v1.18.1
bump compose-go to version v1.18.1
2023-08-10 21:08:07 +02:00
Guillaume Lours
ce3cb2b00c bump compose-go to version v1.18.1
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-08-10 20:51:22 +02:00
Guillaume Lours
d9e73db8e6 Merge pull request #10891 from glours/bump-compose-go-v1.18.0
bump compose-go to version v1.18.0
2023-08-10 17:13:53 +02:00
Guillaume Lours
d6b4d1c755 bump compose-go to version v1.18.0
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-08-10 17:00:32 +02:00
Guillaume Lours
0baf24a269 Merge pull request #10890 from glours/bump-golang-1.21
upgrade Golang to 1.21
2023-08-10 15:22:50 +02:00
Guillaume Lours
0511b0c2b8 Merge pull request #10878 from relrelb/profiles_completion
Add shell completion for `--profile`
2023-08-10 15:16:24 +02:00
Guillaume Lours
5bbdf3d84a Merge pull request #10879 from relrelb/project_directory_completion
Improve shell completion for `--project-directory`
2023-08-10 15:15:58 +02:00
Guillaume Lours
52103cce74 update README and CI workflows to match main branch
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-08-10 15:11:27 +02:00
Guillaume Lours
020b57ca31 upgrade Golang to 1.21
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2023-08-10 15:07:09 +02:00
Milas Bowman
bfa54081d4 build: fix missing proxy build args for classic builder (#10887)
Refactor to use a consistent code path for determining the build
args for a service image regardless of whether BuildKit or the
classic builder is being used.

After recent changes, these code paths had diverged, so the classic
builder was missing the proxy variables from the Docker client
config.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-10 08:57:28 -04:00
Milas Bowman
0be8e4a676 trace: do not block connecting to OTLP endpoint (#10882)
This was left over from debugging, but we should not block.
OTel will handle the connection in the background.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-08 15:47:18 -04:00
Milas Bowman
fd8ab2f7ac watch: enable tar-based syncer by default (#10877)
Swap the default implementation now that batching is merged.
Keeping the `docker cp` based implementation around for the
moment, but it needs to be _explicitly_ disabled now by setting
`COMPOSE_EXPERIMENTAL_WATCH_TAR=0`.

After the next release, we should remove the `docker cp`
implementation entirely.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-04 16:58:01 -04:00
Guillaume Lours
b406b393bf Merge pull request #10881 from silvin-lubecki/display-builder-name
Display builder's name on the first build line.
2023-08-04 17:36:18 +02:00
Silvin Lubecki
0a9d1277c5 Display builder's name on the first build line.
Code borrowed from buildx commands/build.go.

Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2023-08-04 17:11:44 +02:00
Milas Bowman
c350f80d4b up: do not warn on successful optional dependency complete (#10870)
If an optional dependency exits successfully (exit code of 0),
with a service condition of `service_completed_successfully`,
don't log a warning.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-03 21:00:49 +00:00
relrelb
8a4095b507 Improve shell completion for --project-directory
Signed-off-by: Ariel Bachar <relrelb@users.noreply.github.com>
Signed-off-by: relrelb <relrelb@users.noreply.github.com>
2023-08-03 23:40:56 +03:00
relrelb
0345461412 Add shell completion for --profile
Signed-off-by: Ariel Bachar <relrelb@users.noreply.github.com>
Signed-off-by: relrelb <relrelb@users.noreply.github.com>
2023-08-03 23:09:13 +03:00
Milas Bowman
80856eacaf progress: minor correctness fixes (#10871)
* When waiting for dependencies, `select` on the context as well
  as the ticker
* Write multiple progress events "transactionally" (i.e. hold the
  lock for the duration to avoid other events being interleaved)
* Do not change "finished" steps back to "in progress" to prevent
  flickering

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-03 15:14:17 -04:00
Praful Gupta
d7b1972d5e doc: update Config() comment in API Service interface (#10840)
Update Config comment in Service interface

Signed-off-by: Praful Gupta <prafulgupta6@gmail.com>
2023-08-03 15:13:26 -04:00
Silvin Lubecki
7c42776770 Improve buildkit node creation (#10843)
Move builder and nodes initialization code up, avoiding to recreate/load them for every service build.

Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2023-08-03 15:11:16 -04:00
Milas Bowman
3b0742fd57 watch: batch & de-duplicate file events (#10865)
Adjust the debouncing logic so that it applies to all inbound file
events, regardless of whether they match a sync or rebuild rule.

When the batch is flushed out, if any event for the service is a
rebuild event, then the service is rebuilt and all sync events for
the batch are ignored. If _all_ events in the batch are sync events,
then a sync is triggered, passing the entire batch at once. This
provides a substantial performance win for the new `tar`-based
implementation, as it can efficiently transfer the changes in bulk.

Additionally, this helps with jitter, e.g. it's not uncommon for
there to be double-writes in quick succession to a file, so even if
there's not many files being modified at once, it can still prevent
some unnecessary transfers.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-03 14:53:02 -04:00
Milas Bowman
efd44de1b7 watch: support multiple containers for tar implementation (#10860)
Support services with scale > 1 for the tar watch sync.

Add a "lossy" multi-writer specific to pipes that writes the
tar data to each `io.PipeWriter`, which is connected to `stdin`
for the `tar` process being exec'd in the container.

The data is written serially to each writer. This could be
adjusted to do concurrent writes but that will rapidly increase
the I/O load, so is not done here - in general, 99% of the
time you'll be developing (and thus using watch/sync) with a
single replica of a service.

If a write fails, the corresponding `io.PipeWriter` is removed
from the active set and closed with an error.

This means that a single container copy failing won't stop
writes to the others that are succeeding. Of course, they will
be in an inconsistent state afterwards still, but that's a
different problem.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-03 14:52:39 -04:00
Milas Bowman
bdb3f91eb4 test: temporarily disable an exit-code-from Cucumber test case (#10875)
Something is wrong here, disabling while we investigate.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-03 14:49:59 -04:00
Milas Bowman
f94cb49062 test: fix e2e test for privileged builds (#10873)
We cannot guarantee the exact value of `CapEff` across
environments, and this test has started failing some places,
e.g. Docker Desktop, and now GitHub Actions (likely due to
a kernel upgrade on the runners or similar).

By setting `privileged: true` on the build, we're asking for
the `security.insecure` entitlement on the build. A safe
assumption is that will include `CAP_SYS_ADMIN`, which won't
be present otherwise, so mask the `CapEff` value and check
for that.

It's worth noting that realistically, the build won't even
be able to complete without the correct entitlement, since the
`Dockerfile` uses `RUN --security=insecure`, so this is really
an additional sanity check.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-03 12:23:24 -04:00
Milas Bowman
e7ed070690 Merge pull request #10861 from thaJeztah/update_go1.20.7
update to go1.20.7
2023-08-02 10:08:37 -04:00
Sebastiaan van Stijn
8a1bf5d28b update to go1.20.7
Includes a fix for CVE-2023-29409

go1.20.7 (released 2023-08-01) includes a security fix to the crypto/tls
package, as well as bug fixes to the assembler and the compiler. See the
Go 1.20.7 milestone on our issue tracker for details:

- https://github.com/golang/go/issues?q=milestone%3AGo1.20.7+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.20.6...go1.20.7

From the mailing list announcement:

[security] Go 1.20.7 and Go 1.19.12 are released

Hello gophers,

We have just released Go versions 1.20.7 and 1.19.12, minor point releases.

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

- crypto/tls: restrict RSA keys in certificates to <= 8192 bits

  Extremely large RSA keys in certificate chains can cause a client/server
  to expend significant CPU time verifying signatures. Limit this by
  restricting the size of RSA keys transmitted during handshakes to <=
  8192 bits.

  Based on a survey of publicly trusted RSA keys, there are currently only
  three certificates in circulation with keys larger than this, and all
  three appear to be test certificates that are not actively deployed. It
  is possible there are larger keys in use in private PKIs, but we target
  the web PKI, so causing breakage here in the interests of increasing the
  default safety of users of crypto/tls seems reasonable.

  Thanks to Mateusz Poliwczak for reporting this issue.

View the release notes for more information:
https://go.dev/doc/devel/release#go1.20.7

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-08-02 00:22:13 +02:00
dependabot[bot]
7ef392004f build(deps): bump github.com/docker/docker from 24.0.5-0.20230714235725-36e9e796c6fc+incompatible to 24.0.5+incompatible (#10844)
build(deps): bump github.com/docker/docker

Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.5-0.20230714235725-36e9e796c6fc+incompatible to 24.0.5+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/commits/v24.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 18:46:15 +00:00
dependabot[bot]
f34f5b4d26 build(deps): bump github.com/containerd/containerd from 1.7.2 to 1.7.3 (#10850)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.2...v1.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 14:41:57 -04:00
dependabot[bot]
b0484700da build(deps): bump google.golang.org/grpc from 1.56.2 to 1.57.0 (#10847)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.56.2 to 1.57.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.56.2...v1.57.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 14:40:31 -04:00
Milas Bowman
f65fd02383 watch: add tar sync implementation (#10853)
Brought to you by Tilt ❤️ 

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-01 14:39:08 -04:00
Milas Bowman
cf8dc46560 Merge pull request #10845 from docker/dependabot/go_modules/github.com/docker/cli-24.0.5incompatible
build(deps): bump github.com/docker/cli from 24.0.4+incompatible to 24.0.5+incompatible
2023-08-01 14:29:27 -04:00
dependabot[bot]
2cfbe63533 build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 24.0.4+incompatible to 24.0.5+incompatible.
- [Commits](https://github.com/docker/cli/compare/v24.0.4...v24.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-25 09:25:44 +00:00
Guillaume Lours
8318f66330 Merge pull request #10791 from milas/watch-refactor-sync
watch: move sync logic into separate package
2023-07-19 13:11:24 +02:00
Milas Bowman
cb17c3c8a6 watch: move sync logic into separate package
Just moving some code around in preparation for an alternative
sync implementation that can do bulk transfers by using `tar`.

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

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

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

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

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

Fixes #10813.

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

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

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

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

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

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

full diff:

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

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

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

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

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

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

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

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

net/http: insufficient sanitization of Host header

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

Thanks to Bartek Nowotarski for reporting this issue.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

View File

@@ -7,7 +7,7 @@ concurrency:
on:
push:
branches:
- 'v2'
- 'main'
tags:
- 'v*'
pull_request:

View File

@@ -7,7 +7,7 @@ concurrency:
on:
push:
branches:
- 'v2'
- 'main'
tags:
- 'v*'

View File

@@ -5,7 +5,7 @@ on:
schedule:
- cron: '44 9 * * 4'
push:
branches: [ "v2" ]
branches: [ "main" ]
# Declare default permissions as read only.
permissions: read-all

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.5
ARG GO_VERSION=1.21.0
ARG XX_VERSION=1.2.1
ARG GOLANGCI_LINT_VERSION=v1.53.2
ARG GOLANGCI_LINT_VERSION=v1.54.2
ARG ADDLICENSE_VERSION=v1.0.0
ARG BUILD_TAGS="e2e"
@@ -89,9 +89,13 @@ RUN --mount=type=bind,target=. \
FROM build-base AS lint
ARG BUILD_TAGS
ENV GOLANGCI_LINT_CACHE=/cache/golangci-lint
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/cache/golangci-lint \
--mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
golangci-lint cache status && \
golangci-lint run --build-tags "$BUILD_TAGS" ./...
FROM build-base AS test
@@ -129,6 +133,7 @@ FROM base AS docsgen
WORKDIR /src
RUN --mount=target=. \
--mount=target=/root/.cache,type=cache \
--mount=type=cache,target=/go/pkg/mod \
go build -o /out/docsgen ./docs/yaml/main/generate.go
FROM --platform=${BUILDPLATFORM} alpine AS docs-build

View File

@@ -6,13 +6,14 @@
+ [Linux](#linux)
- [Quick Start](#quick-start)
- [Contributing](#contributing)
- [Legacy](#legacy)
# Docker Compose v2
[![GitHub release](https://img.shields.io/github/release/docker/compose.svg?style=flat-square)](https://github.com/docker/compose/releases/latest)
[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?style=flat-square&logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/compose/v2)
[![Build Status](https://img.shields.io/github/workflow/status/docker/compose/ci?label=ci&logo=github&style=flat-square)](https://github.com/docker/compose/actions?query=workflow%3Aci)
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/compose/ci.yml?label=ci&logo=github&style=flat-square)](https://github.com/docker/compose/actions?query=workflow%3Aci)
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/compose/v2?style=flat-square)](https://goreportcard.com/report/github.com/docker/compose/v2)
[![Codecov](https://codecov.io/gh/docker/compose/branch/master/graph/badge.svg?token=HP3K4Y4ctu)](https://codecov.io/gh/docker/compose)
[![Codecov](https://codecov.io/gh/docker/compose/branch/main/graph/badge.svg?token=HP3K4Y4ctu)](https://codecov.io/gh/docker/compose)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/docker/compose/badge)](https://api.securityscorecards.dev/projects/github.com/docker/compose)
![Docker Compose](logo.png?raw=true "Docker Compose Logo")
@@ -23,12 +24,6 @@ your application are configured.
Once you have a Compose file, you can create and start your application with a
single command: `docker compose up`.
# About update and backward compatibility
Docker Compose V2 is a major version bump release of Docker Compose. It has been completely rewritten from scratch in Golang (V1 was in Python). The installation instructions for Compose V2 differ from V1. V2 is not a standalone binary anymore, and installation scripts will have to be adjusted. Some commands are different.
For a smooth transition from legacy docker-compose 1.xx, please consider installing [compose-switch](https://github.com/docker/compose-switch) to translate `docker-compose ...` commands into Compose V2's `docker compose .... `. Also check V2's `--compatibility` flag.
# Where to get Docker Compose
### Windows and macOS
@@ -85,3 +80,8 @@ Want to help develop Docker Compose? Check out our
If you find an issue, please report it on the
[issue tracker](https://github.com/docker/compose/issues/new/choose).
Legacy
-------------
The Python version of Compose is available under the `v1` [branch](https://github.com/docker/compose/tree/v1).

View File

@@ -17,6 +17,7 @@
package compose
import (
"sort"
"strings"
"github.com/docker/compose/v2/pkg/api"
@@ -65,3 +66,23 @@ func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []s
return values, cobra.ShellCompDirectiveNoFileComp
}
}
func completeProfileNames(p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
project, err := p.ToProject(nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
allProfileNames := project.AllServices().GetProfiles()
sort.Strings(allProfileNames)
var values []string
for _, profileName := range allProfileNames {
if strings.HasPrefix(profileName, toComplete) {
values = append(values, profileName)
}
}
return values, cobra.ShellCompDirectiveNoFileComp
}
}

View File

@@ -26,8 +26,10 @@ import (
"strings"
"syscall"
"github.com/compose-spec/compose-go/dotenv"
buildx "github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/remote"
"github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types"
@@ -133,7 +135,25 @@ func (o *ProjectOptions) WithProject(fn ProjectFunc) func(cmd *cobra.Command, ar
// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
func (o *ProjectOptions) WithServices(fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
return Adapt(func(ctx context.Context, args []string) error {
project, err := o.ToProject(args, cli.WithResolvedPaths(true), cli.WithDiscardEnvFile)
options := []cli.ProjectOptionsFn{
cli.WithResolvedPaths(true),
cli.WithDiscardEnvFile,
cli.WithContext(ctx),
}
enabled, err := remote.GitRemoteLoaderEnabled()
if err != nil {
return err
}
if enabled {
git, err := remote.NewGitRemoteLoader()
if err != nil {
return err
}
options = append(options, cli.WithResourceLoader(git))
}
project, err := o.ToProject(args, options...)
if err != nil {
return err
}
@@ -242,7 +262,7 @@ func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.Proj
cli.WithDotEnv,
cli.WithConfigFileEnv,
cli.WithDefaultConfigPath,
cli.WithProfiles(o.Profiles),
cli.WithDefaultProfiles(o.Profiles...),
cli.WithName(o.ProjectName))...)
}
@@ -255,7 +275,7 @@ func RunningAsStandalone() bool {
}
// RootCommand returns the compose command with its child commands
func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo
func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo
// filter out useless commandConn.CloseWrite warning message that can occur
// when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
@@ -287,7 +307,7 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
return cmd.Help()
}
if version {
return versionCommand(streams).Execute()
return versionCommand(dockerCli).Execute()
}
_ = cmd.Help()
return dockercli.StatusError{
@@ -325,11 +345,11 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
ansi = v
}
formatter.SetANSIMode(streams, ansi)
formatter.SetANSIMode(dockerCli, ansi)
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
ui.NoColor()
formatter.SetANSIMode(streams, formatter.Never)
formatter.SetANSIMode(dockerCli, formatter.Never)
}
switch ansi {
@@ -406,26 +426,26 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
}
c.AddCommand(
upCommand(&opts, streams, backend),
upCommand(&opts, dockerCli, backend),
downCommand(&opts, backend),
startCommand(&opts, backend),
restartCommand(&opts, backend),
stopCommand(&opts, backend),
psCommand(&opts, streams, backend),
listCommand(streams, backend),
logsCommand(&opts, streams, backend),
configCommand(&opts, streams, backend),
psCommand(&opts, dockerCli, backend),
listCommand(dockerCli, backend),
logsCommand(&opts, dockerCli, backend),
configCommand(&opts, dockerCli, backend),
killCommand(&opts, backend),
runCommand(&opts, streams, backend),
runCommand(&opts, dockerCli, backend),
removeCommand(&opts, backend),
execCommand(&opts, streams, backend),
execCommand(&opts, dockerCli, backend),
pauseCommand(&opts, backend),
unpauseCommand(&opts, backend),
topCommand(&opts, streams, backend),
eventsCommand(&opts, streams, backend),
portCommand(&opts, streams, backend),
imagesCommand(&opts, streams, backend),
versionCommand(streams),
topCommand(&opts, dockerCli, backend),
eventsCommand(&opts, dockerCli, backend),
portCommand(&opts, dockerCli, backend),
imagesCommand(&opts, dockerCli, backend),
versionCommand(dockerCli),
buildCommand(&opts, &progress, backend),
pushCommand(&opts, backend),
pullCommand(&opts, backend),
@@ -441,12 +461,22 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
"project-name",
completeProjectNames(backend),
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"project-directory",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{}, cobra.ShellCompDirectiveFilterDirs
},
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"file",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"yaml", "yml"}, cobra.ShellCompDirectiveFilterFileExt
},
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"profile",
completeProfileNames(&opts),
)
c.Flags().StringVar(&progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
@@ -472,7 +502,7 @@ func setEnvWithDotEnv(prjOpts *ProjectOptions) error {
return err
}
envFromFile, err := cli.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), workingDir, options.EnvFiles)
envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), workingDir, options.EnvFiles)
if err != nil {
return err
}

View File

@@ -26,6 +26,7 @@ import (
"github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/remote"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
@@ -49,14 +50,21 @@ type configOptions struct {
noConsistency bool
}
func (o *configOptions) ToProject(services []string) (*types.Project, error) {
func (o *configOptions) ToProject(ctx context.Context, services []string) (*types.Project, error) {
git, err := remote.NewGitRemoteLoader()
if err != nil {
return nil, err
}
return o.ProjectOptions.ToProject(services,
cli.WithInterpolation(!o.noInterpolate),
cli.WithResolvedPaths(!o.noResolvePath),
cli.WithNormalization(!o.noNormalize),
cli.WithConsistency(!o.noConsistency),
cli.WithProfiles(o.Profiles),
cli.WithDiscardEnvFile)
cli.WithDefaultProfiles(o.Profiles...),
cli.WithDiscardEnvFile,
cli.WithContext(ctx),
cli.WithResourceLoader(git))
}
func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
@@ -82,19 +90,19 @@ func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service)
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
if opts.services {
return runServices(streams, opts)
return runServices(ctx, streams, opts)
}
if opts.volumes {
return runVolumes(streams, opts)
return runVolumes(ctx, streams, opts)
}
if opts.hash != "" {
return runHash(streams, opts)
return runHash(ctx, streams, opts)
}
if opts.profiles {
return runProfiles(streams, opts, args)
return runProfiles(ctx, streams, opts, args)
}
if opts.images {
return runConfigImages(streams, opts, args)
return runConfigImages(ctx, streams, opts, args)
}
return runConfig(ctx, streams, backend, opts, args)
@@ -122,7 +130,7 @@ func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service)
func runConfig(ctx context.Context, streams api.Streams, backend api.Service, opts configOptions, services []string) error {
var content []byte
project, err := opts.ToProject(services)
project, err := opts.ToProject(ctx, services)
if err != nil {
return err
}
@@ -151,8 +159,8 @@ func runConfig(ctx context.Context, streams api.Streams, backend api.Service, op
return err
}
func runServices(streams api.Streams, opts configOptions) error {
project, err := opts.ToProject(nil)
func runServices(ctx context.Context, streams api.Streams, opts configOptions) error {
project, err := opts.ToProject(ctx, nil)
if err != nil {
return err
}
@@ -162,8 +170,8 @@ func runServices(streams api.Streams, opts configOptions) error {
})
}
func runVolumes(streams api.Streams, opts configOptions) error {
project, err := opts.ToProject(nil)
func runVolumes(ctx context.Context, streams api.Streams, opts configOptions) error {
project, err := opts.ToProject(ctx, nil)
if err != nil {
return err
}
@@ -173,12 +181,12 @@ func runVolumes(streams api.Streams, opts configOptions) error {
return nil
}
func runHash(streams api.Streams, opts configOptions) error {
func runHash(ctx context.Context, streams api.Streams, opts configOptions) error {
var services []string
if opts.hash != "*" {
services = append(services, strings.Split(opts.hash, ",")...)
}
project, err := opts.ToProject(nil)
project, err := opts.ToProject(ctx, nil)
if err != nil {
return err
}
@@ -205,9 +213,9 @@ func runHash(streams api.Streams, opts configOptions) error {
return nil
}
func runProfiles(streams api.Streams, opts configOptions, services []string) error {
func runProfiles(ctx context.Context, streams api.Streams, opts configOptions, services []string) error {
set := map[string]struct{}{}
project, err := opts.ToProject(services)
project, err := opts.ToProject(ctx, services)
if err != nil {
return err
}
@@ -227,17 +235,13 @@ func runProfiles(streams api.Streams, opts configOptions, services []string) err
return nil
}
func runConfigImages(streams api.Streams, opts configOptions, services []string) error {
project, err := opts.ToProject(services)
func runConfigImages(ctx context.Context, streams api.Streams, opts configOptions, services []string) error {
project, err := opts.ToProject(ctx, services)
if err != nil {
return err
}
for _, s := range project.Services {
if s.Image != "" {
fmt.Fprintln(streams.Out(), s.Image)
} else {
fmt.Fprintf(streams.Out(), "%s%s%s\n", project.Name, api.Separator, s.Name)
}
fmt.Fprintln(streams.Out(), api.GetImageNameOrDefault(s, project.Name))
}
return nil
}

View File

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

View File

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

View File

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

View File

@@ -19,22 +19,18 @@ package compose
import (
"context"
"fmt"
"io"
"sort"
"strconv"
"strings"
"time"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/api/types"
formatter2 "github.com/docker/cli/cli/command/formatter"
"github.com/docker/go-units"
"github.com/docker/cli/cli/command"
cliformatter "github.com/docker/cli/cli/command/formatter"
cliflags "github.com/docker/cli/cli/flags"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
)
type psOptions struct {
@@ -66,7 +62,7 @@ func (p *psOptions) parseFilter() error {
return nil
}
func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
opts := psOptions{
ProjectOptions: p,
}
@@ -77,12 +73,12 @@ func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
return opts.parseFilter()
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPs(ctx, streams, backend, args, opts)
return runPs(ctx, dockerCli, backend, args, opts)
}),
ValidArgsFunction: completeServiceNames(p),
}
flags := psCmd.Flags()
flags.StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]")
flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp)
flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status).")
flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
@@ -91,7 +87,7 @@ func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
return psCmd
}
func runPs(ctx context.Context, streams api.Streams, backend api.Service, services []string, opts psOptions) error {
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
project, name, err := opts.projectOrName(services...)
if err != nil {
return err
@@ -125,38 +121,32 @@ func runPs(ctx context.Context, streams api.Streams, backend api.Service, servic
if opts.Quiet {
for _, c := range containers {
fmt.Fprintln(streams.Out(), c.ID)
fmt.Fprintln(dockerCli.Out(), c.ID)
}
return nil
}
if opts.Services {
services := []string{}
for _, s := range containers {
if !utils.StringContains(services, s.Service) {
services = append(services, s.Service)
for _, c := range containers {
s := c.Service
if !utils.StringContains(services, s) {
services = append(services, s)
}
}
fmt.Fprintln(streams.Out(), strings.Join(services, "\n"))
fmt.Fprintln(dockerCli.Out(), strings.Join(services, "\n"))
return nil
}
return formatter.Print(containers, opts.Format, streams.Out(),
writer(containers),
"NAME", "IMAGE", "COMMAND", "SERVICE", "CREATED", "STATUS", "PORTS")
}
func writer(containers []api.ContainerSummary) func(w io.Writer) {
return func(w io.Writer) {
for _, container := range containers {
ports := displayablePorts(container)
createdAt := time.Unix(container.Created, 0)
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
status := container.Status
command := formatter2.Ellipsis(container.Command, 20)
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", container.Name, container.Image, strconv.Quote(command), container.Service, created, status, ports)
}
if opts.Format == "" {
opts.Format = dockerCli.ConfigFile().PsFormat
}
containerCtx := cliformatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewContainerFormat(opts.Format, opts.Quiet, false),
}
return formatter.ContainerWrite(containerCtx, containers)
}
func filterByStatus(containers []api.ContainerSummary, statuses []string) []api.ContainerSummary {
@@ -177,21 +167,3 @@ func hasStatus(c api.ContainerSummary, statuses []string) bool {
}
return false
}
func displayablePorts(c api.ContainerSummary) string {
if c.Publishers == nil {
return ""
}
ports := make([]types.Port, len(c.Publishers))
for i, pub := range c.Publishers {
ports[i] = types.Port{
IP: pub.URL,
PrivatePort: uint16(pub.TargetPort),
PublicPort: uint16(pub.PublishedPort),
Type: pub.Protocol,
}
}
return formatter2.DisplayablePorts(ports)
}

View File

@@ -18,11 +18,11 @@ package compose
import (
"context"
"io"
"os"
"path/filepath"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
@@ -69,7 +69,11 @@ func TestPsTable(t *testing.T) {
}).AnyTimes()
opts := psOptions{ProjectOptions: &ProjectOptions{ProjectName: "test"}}
err = runPs(ctx, stream{out: streams.NewOut(f)}, backend, nil, opts)
stdout := streams.NewOut(f)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(stdout).AnyTimes()
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
err = runPs(ctx, cli, backend, nil, opts)
assert.NoError(t, err)
_, err = f.Seek(0, 0)
@@ -80,21 +84,3 @@ func TestPsTable(t *testing.T) {
assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
}
type stream struct {
out *streams.Out
err io.Writer
in *streams.In
}
func (s stream) Out() *streams.Out {
return s.out
}
func (s stream) Err() io.Writer {
return s.err
}
func (s stream) In() *streams.In {
return s.in
}

View File

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

View File

@@ -18,12 +18,13 @@ package compose
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
@@ -84,7 +85,10 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
create.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
if create.ignoreOrphans && create.removeOrphans {
return fmt.Errorf("%s and --remove-orphans cannot be combined", ComposeIgnoreOrphans)
return fmt.Errorf("cannot combine %s and --remove-orphans", ComposeIgnoreOrphans)
}
if len(up.attach) != 0 && up.attachDependencies {
return errors.New("cannot combine --attach and --attach-dependencies")
}
return runUp(ctx, streams, backend, create, up, project, services)
}),
@@ -109,12 +113,12 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services.")
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers.")
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Attach to dependent containers.")
flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information.")
flags.StringArrayVar(&up.attach, "attach", []string{}, "Attach to service output.")
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Don't attach to specified service.")
flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.")
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services.")
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services.")
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "timeout waiting for application to be running|healthy.")
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration to wait for the project to be running|healthy.")
return upCmd
}
@@ -159,23 +163,6 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
return err
}
var consumer api.LogConsumer
if !upOptions.Detach {
consumer = formatter.NewLogConsumer(ctx, streams.Out(), streams.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)
}
attachTo := services
if len(upOptions.attach) > 0 {
attachTo = upOptions.attach
}
if upOptions.attachDependencies {
attachTo = project.ServiceNames()
}
if len(attachTo) == 0 {
attachTo = project.ServiceNames()
}
attachTo = utils.Remove(attachTo, upOptions.noAttach...)
create := api.CreateOptions{
Services: services,
RemoveOrphans: createOptions.removeOrphans,
@@ -191,14 +178,48 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
return backend.Create(ctx, project, create)
}
timeout := time.Duration(upOptions.waitTimeout) * time.Second
var consumer api.LogConsumer
var attach []string
if !upOptions.Detach {
consumer = formatter.NewLogConsumer(ctx, streams.Out(), streams.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)
var attachSet utils.Set[string]
if len(upOptions.attach) != 0 {
// services are passed explicitly with --attach, verify they're valid and then use them as-is
attachSet = utils.NewSet(upOptions.attach...)
unexpectedSvcs := attachSet.Diff(utils.NewSet(project.ServiceNames()...))
if len(unexpectedSvcs) != 0 {
return fmt.Errorf("cannot attach to services not included in up: %s", strings.Join(unexpectedSvcs.Elements(), ", "))
}
} else {
// mark services being launched (and potentially their deps) for attach
// if they didn't opt-out via Compose YAML
attachSet = utils.NewSet[string]()
var dependencyOpt types.DependencyOption = types.IgnoreDependencies
if upOptions.attachDependencies {
dependencyOpt = types.IncludeDependencies
}
if err := project.WithServices(services, func(s types.ServiceConfig) error {
if s.Attach == nil || *s.Attach {
attachSet.Add(s.Name)
}
return nil
}, dependencyOpt); err != nil {
return err
}
}
// filter out any services that have been explicitly marked for ignore with `--no-attach`
attachSet.RemoveAll(upOptions.noAttach...)
attach = attachSet.Elements()
}
timeout := time.Duration(upOptions.waitTimeout) * time.Second
return backend.Up(ctx, project, api.UpOptions{
Create: create,
Start: api.StartOptions{
Project: project,
Attach: consumer,
AttachTo: attachTo,
AttachTo: attach,
ExitCodeFrom: upOptions.exitCodeFrom,
CascadeStop: upOptions.cascadeStop,
Wait: upOptions.wait,

View File

@@ -21,6 +21,8 @@ import (
"fmt"
"os"
"github.com/docker/compose/v2/internal/locker"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
@@ -57,5 +59,13 @@ func runWatch(ctx context.Context, backend api.Service, opts watchOptions, servi
return err
}
l, err := locker.NewPidfile(project.Name)
if err != nil {
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
}
if err := l.Lock(); err != nil {
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
}
return backend.Watch(ctx, project, services, api.WatchOptions{})
}

281
cmd/formatter/container.go Normal file
View File

@@ -0,0 +1,281 @@
/*
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 formatter
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
const (
defaultContainerTableFormat = "table {{.Name}}\t{{.Image}}\t{{.Command}}\t{{.Service}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}"
nameHeader = "NAME"
serviceHeader = "SERVICE"
commandHeader = "COMMAND"
runningForHeader = "CREATED"
mountsHeader = "MOUNTS"
localVolumes = "LOCAL VOLUMES"
networksHeader = "NETWORKS"
)
// NewContainerFormat returns a Format for rendering using a Context
func NewContainerFormat(source string, quiet bool, size bool) formatter.Format {
switch source {
case formatter.TableFormatKey, "": // table formatting is the default if none is set.
if quiet {
return formatter.DefaultQuietFormat
}
format := defaultContainerTableFormat
if size {
format += `\t{{.Size}}`
}
return formatter.Format(format)
case formatter.RawFormatKey:
if quiet {
return `container_id: {{.ID}}`
}
format := `container_id: {{.ID}}
image: {{.Image}}
command: {{.Command}}
created_at: {{.CreatedAt}}
state: {{- pad .State 1 0}}
status: {{- pad .Status 1 0}}
names: {{.Names}}
labels: {{- pad .Labels 1 0}}
ports: {{- pad .Ports 1 0}}
`
if size {
format += `size: {{.Size}}\n`
}
return formatter.Format(format)
default: // custom format
if quiet {
return formatter.DefaultQuietFormat
}
return formatter.Format(source)
}
}
// ContainerWrite renders the context for a list of containers
func ContainerWrite(ctx formatter.Context, containers []api.ContainerSummary) error {
render := func(format func(subContext formatter.SubContext) error) error {
for _, container := range containers {
err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
if err != nil {
return err
}
}
return nil
}
return ctx.Write(NewContainerContext(), render)
}
// ContainerContext is a struct used for rendering a list of containers in a Go template.
type ContainerContext struct {
formatter.HeaderContext
trunc bool
c api.ContainerSummary
// FieldsUsed is used in the pre-processing step to detect which fields are
// used in the template. It's currently only used to detect use of the .Size
// field which (if used) automatically sets the '--size' option when making
// the API call.
FieldsUsed map[string]interface{}
}
// NewContainerContext creates a new context for rendering containers
func NewContainerContext() *ContainerContext {
containerCtx := ContainerContext{}
containerCtx.Header = formatter.SubHeaderContext{
"ID": formatter.ContainerIDHeader,
"Name": nameHeader,
"Service": serviceHeader,
"Image": formatter.ImageHeader,
"Command": commandHeader,
"CreatedAt": formatter.CreatedAtHeader,
"RunningFor": runningForHeader,
"Ports": formatter.PortsHeader,
"State": formatter.StateHeader,
"Status": formatter.StatusHeader,
"Size": formatter.SizeHeader,
"Labels": formatter.LabelsHeader,
}
return &containerCtx
}
// MarshalJSON makes ContainerContext implement json.Marshaler
func (c *ContainerContext) MarshalJSON() ([]byte, error) {
return formatter.MarshalJSON(c)
}
// ID returns the container's ID as a string. Depending on the `--no-trunc`
// option being set, the full or truncated ID is returned.
func (c *ContainerContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.c.ID)
}
return c.c.ID
}
func (c *ContainerContext) Name() string {
return c.c.Name
}
// Names returns a comma-separated string of the container's names, with their
// slash (/) prefix stripped. Additional names for the container (related to the
// legacy `--link` feature) are omitted.
func (c *ContainerContext) Names() string {
names := formatter.StripNamePrefix(c.c.Names)
if c.trunc {
for _, name := range names {
if len(strings.Split(name, "/")) == 1 {
names = []string{name}
break
}
}
}
return strings.Join(names, ",")
}
func (c *ContainerContext) Service() string {
return c.c.Service
}
func (c *ContainerContext) Image() string {
return c.c.Image
}
func (c *ContainerContext) Command() string {
command := c.c.Command
if c.trunc {
command = formatter.Ellipsis(command, 20)
}
return strconv.Quote(command)
}
func (c *ContainerContext) CreatedAt() string {
return time.Unix(c.c.Created, 0).String()
}
func (c *ContainerContext) RunningFor() string {
createdAt := time.Unix(c.c.Created, 0)
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
}
func (c *ContainerContext) ExitCode() int {
return c.c.ExitCode
}
func (c *ContainerContext) State() string {
return c.c.State
}
func (c *ContainerContext) Status() string {
return c.c.Status
}
func (c *ContainerContext) Health() string {
return c.c.Health
}
func (c *ContainerContext) Publishers() api.PortPublishers {
return c.c.Publishers
}
func (c *ContainerContext) Ports() string {
var ports []types.Port
for _, publisher := range c.c.Publishers {
ports = append(ports, types.Port{
IP: publisher.URL,
PrivatePort: uint16(publisher.TargetPort),
PublicPort: uint16(publisher.PublishedPort),
Type: publisher.Protocol,
})
}
return formatter.DisplayablePorts(ports)
}
// Labels returns a comma-separated string of labels present on the container.
func (c *ContainerContext) Labels() string {
if c.c.Labels == nil {
return ""
}
var joinLabels []string
for k, v := range c.c.Labels {
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(joinLabels, ",")
}
// Label returns the value of the label with the given name or an empty string
// if the given label does not exist.
func (c *ContainerContext) Label(name string) string {
if c.c.Labels == nil {
return ""
}
return c.c.Labels[name]
}
// Mounts returns a comma-separated string of mount names present on the container.
// If the trunc option is set, names can be truncated (ellipsized).
func (c *ContainerContext) Mounts() string {
var mounts []string
for _, name := range c.c.Mounts {
if c.trunc {
name = formatter.Ellipsis(name, 15)
}
mounts = append(mounts, name)
}
return strings.Join(mounts, ",")
}
// LocalVolumes returns the number of volumes using the "local" volume driver.
func (c *ContainerContext) LocalVolumes() string {
return fmt.Sprintf("%d", c.c.LocalVolumes)
}
// Networks returns a comma-separated string of networks that the container is
// attached to.
func (c *ContainerContext) Networks() string {
return strings.Join(c.c.Networks, ",")
}
// Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
func (c *ContainerContext) Size() string {
if c.FieldsUsed == nil {
c.FieldsUsed = map[string]interface{}{}
}
c.FieldsUsed["Size"] = struct{}{}
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
sf := srw
if c.c.SizeRootFs > 0 {
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
}
return sf
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,15 +5,15 @@ List containers
### Options
| Name | Type | Default | Description |
|:----------------------|:--------------|:--------|:--------------------------------------------------------------------------------------------------------------|
| `-a`, `--all` | | | Show all stopped containers (including those created by the run command) |
| `--dry-run` | | | Execute command in dry run mode |
| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). |
| [`--format`](#format) | `string` | `table` | Format the output. Values: [table \| json] |
| `-q`, `--quiet` | | | Only display IDs |
| `--services` | | | Display services |
| [`--status`](#status) | `stringArray` | | Filter services by status. Values: [paused \| restarting \| removing \| running \| dead \| created \| exited] |
| Name | Type | Default | Description |
|:----------------------|:--------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `-a`, `--all` | | | Show all stopped containers (including those created by the run command) |
| `--dry-run` | | | Execute command in dry run mode |
| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). |
| [`--format`](#format) | `string` | `table` | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `-q`, `--quiet` | | | Only display IDs |
| `--services` | | | Display services |
| [`--status`](#status) | `stringArray` | | Filter services by status. Values: [paused \| restarting \| removing \| running \| dead \| created \| exited] |
<!---MARKER_GEN_END-->

View File

@@ -9,14 +9,14 @@ Create and start containers
|:-----------------------------|:--------------|:----------|:---------------------------------------------------------------------------------------------------------|
| `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d |
| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. |
| `--attach` | `stringArray` | | Attach to service output. |
| `--attach-dependencies` | | | Attach to dependent containers. |
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
| `--attach-dependencies` | | | Automatically attach to log output of dependent services. |
| `--build` | | | Build images before starting containers. |
| `-d`, `--detach` | | | Detached mode: Run containers in the background |
| `--dry-run` | | | Execute command in dry run mode |
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed. |
| `--no-attach` | `stringArray` | | Don't attach to specified service. |
| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services. |
| `--no-build` | | | Don't build an image, even if it's missing. |
| `--no-color` | | | Produce monochrome output. |
| `--no-deps` | | | Don't start linked services. |
@@ -31,7 +31,7 @@ Create and start containers
| `-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. |
| `--wait-timeout` | `int` | `0` | Maximum duration to wait for the project to be running\|healthy. |
<!---MARKER_GEN_END-->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,13 @@ options:
- option: format
value_type: string
default_value: table
description: 'Format the output. Values: [table | json]'
description: |-
Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates
details_url: '#format'
deprecated: false
hidden: false
@@ -180,6 +186,7 @@ examples: |-
The `docker compose ps` command currently only supports the `--filter status=<status>`
option, but additional filter options may be added in the future.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,8 @@ options:
- option: attach
value_type: stringArray
default_value: '[]'
description: Attach to service output.
description: |
Restrict attaching to the specified services. Incompatible with --attach-dependencies.
deprecated: false
hidden: false
experimental: false
@@ -58,7 +59,7 @@ options:
- option: attach-dependencies
value_type: bool
default_value: "false"
description: Attach to dependent containers.
description: Automatically attach to log output of dependent services.
deprecated: false
hidden: false
experimental: false
@@ -110,7 +111,7 @@ options:
- option: no-attach
value_type: stringArray
default_value: '[]'
description: Don't attach to specified service.
description: Do not attach (stream logs) to the specified services.
deprecated: false
hidden: false
experimental: false
@@ -266,7 +267,7 @@ options:
- option: wait-timeout
value_type: int
default_value: "0"
description: timeout waiting for application to be running|healthy.
description: Maximum duration to wait for the project to be running|healthy.
deprecated: false
hidden: false
experimental: false
@@ -285,6 +286,7 @@ inherited_options:
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ Background:
services:
should_fail:
image: alpine
command: ls /does_not_exist
command: ['sh', '-c', 'exit 123']
sleep: # will be killed
image: alpine
command: ping localhost
@@ -15,15 +15,22 @@ Background:
Scenario: Cascade stop
When I run "compose up --abort-on-container-exit"
Then the output contains "should_fail-1 exited with code 1"
Then the output contains "should_fail-1 exited with code 123"
And the output contains "Aborting on container exit..."
And the exit code is 1
And the exit code is 123
Scenario: Exit code from
When I run "compose up --exit-code-from sleep"
Then the output contains "should_fail-1 exited with code 1"
When I run "compose up --exit-code-from should_fail"
Then the output contains "should_fail-1 exited with code 123"
And the output contains "Aborting on container exit..."
And the exit code is 143
And the exit code is 123
# TODO: this is currently not working propagating the exit code properly
#Scenario: Exit code from (cascade stop)
# When I run "compose up --exit-code-from sleep"
# Then the output contains "should_fail-1 exited with code 123"
# And the output contains "Aborting on container exit..."
# And the exit code is 143
Scenario: Exit code from unknown service
When I run "compose up --exit-code-from unknown"

37
go.mod
View File

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

106
go.sum
View File

@@ -26,7 +26,9 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@@ -43,6 +45,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0=
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 h1:+vTEFqeoeur6XSq06bs+roX3YiT49gUniJK7Zky7Xjg=
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
@@ -54,19 +57,25 @@ github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/hcsshim v0.10.0-rc.8 h1:YSZVvlIIDD1UxQpJp0h+dnpLUw+TrY0cx8obKsp3bek=
github.com/Microsoft/hcsshim v0.10.0-rc.8/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go-v2 v1.17.6 h1:Y773UK7OBqhzi5VDXMi1zVGsoj+CVHs2eaC2bDsLwi0=
github.com/aws/aws-sdk-go-v2 v1.17.6/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/config v1.18.16 h1:4r7gsCu8Ekwl5iJGE/GmspA2UifqySCCkyyyPFeWs3w=
@@ -107,6 +116,7 @@ github.com/bugsnag/bugsnag-go v1.5.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqR
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA=
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -130,20 +140,26 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/compose-spec/compose-go v1.15.1 h1:0yaEt6/66dLN0bNWYDTj0CDx626uCdQ9ipJVIJx8O8M=
github.com/compose-spec/compose-go v1.15.1/go.mod h1:3yngGBGfls6FHGQsg4B1z6gz8ej9SOvmAJtxCwgbcnc=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go v1.18.3 h1:hiwTZ8ED1l+CB2G2G4LFv/bIaoUfG2ZBalz4S7MOy5w=
github.com/compose-spec/compose-go v1.18.3/go.mod h1:zR2tP1+kZHi5vJz7PjpW6oMoDji/Js3GHjP+hfjf70Q=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/containerd v1.7.2 h1:UF2gdONnxO8I6byZXDi5sXWiWvlW3D/sci7dTQimEJo=
github.com/containerd/containerd v1.7.2/go.mod h1:afcz74+K10M/+cjGHIVQrCt3RAQhUSCAjJ9iMYhhkuI=
github.com/containerd/containerd v1.7.3 h1:cKwYKkP1eTj54bP3wCdXXBymmKRQMrWjkLSWZZJDa8o=
github.com/containerd/containerd v1.7.3/go.mod h1:32FOM4/O0RkNg7AjQj3hDzN9cUGtu+HMvaKUNiqCZB8=
github.com/containerd/continuity v0.4.1 h1:wQnVrjIyQ8vhU2sgOiL5T07jo+ouqc2bnKsv5/EqGhU=
github.com/containerd/continuity v0.4.1/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/nydus-snapshotter v0.8.2 h1:7SOrMU2YmLzfbsr5J7liMZJlNi5WT6vtIOxLGv+iz7E=
github.com/containerd/nydus-snapshotter v0.8.2/go.mod h1:UJILTN5LVBRY+dt8BGJbp72Xy729hUZsOugObEI3/O8=
github.com/containerd/stargz-snapshotter v0.14.3 h1:OTUVZoPSPs8mGgmQUE1dqw3WX/3nrsmsurW7UPLWl1U=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs=
github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak=
github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4=
@@ -153,6 +169,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw=
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
@@ -167,17 +184,17 @@ github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zA
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493 h1:fm5DpBD+A7o0+x9Nf+o9/4/qPGbfxLpr9qIPVuV8vQc=
github.com/distribution/distribution/v3 v3.0.0-20230601133803-97b1d649c493/go.mod h1:+fqBJ4vPYo4Uu1ZE4d+bUtTLRXfdSL3NvCZIZ9GHv58=
github.com/docker/buildx v0.11.0 h1:DNCOIYT/7J0sPBlU/ozEhFd4MtbnbFByn45yeTMHXVU=
github.com/docker/buildx v0.11.0/go.mod h1:Yq7ZNjrwXKzW0uSFMk46dl5Gl903k5+bp6U4apsM5rs=
github.com/docker/cli v24.0.2+incompatible h1:QdqR7znue1mtkXIJ+ruQMGQhpw2JzMJLRXp6zpzF6tM=
github.com/docker/cli v24.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.5.1 h1:jIk/cCZurZERhALPVKhqlNxTQGxn2kcI+56gE57PQXg=
github.com/docker/cli-docs-tool v0.5.1/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o=
github.com/docker/buildx v0.11.2 h1:R3p9F0gnI4FwvQ0p40UwdX1T4ugap4UWxY3TFHoP4Ws=
github.com/docker/buildx v0.11.2/go.mod h1:CWAABt10iIuGpleypA3103mplDfcGu0A2AvT03xfpTc=
github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc=
github.com/docker/cli v24.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.6.0 h1:Z9x10SaZgFaB6jHgz3OWooynhSa40CsWkpe5hEnG/qA=
github.com/docker/cli-docs-tool v0.6.0/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.2+incompatible h1:eATx+oLz9WdNVkQrr0qjQ8HvRJ4bOOxfzEo8R+dA3cg=
github.com/docker/docker v24.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@@ -185,6 +202,7 @@ github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
@@ -198,6 +216,7 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8=
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -209,6 +228,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8=
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
@@ -216,6 +236,7 @@ github.com/fsnotify/fsevents v0.1.1 h1:/125uxJvvoSDDBPen6yUZbil8J9ydKZnnl3TWWmvn
github.com/fsnotify/fsevents v0.1.1/go.mod h1:+d+hS27T6k5J8CRaPLKFgwKYcpS7GwW3Ule9+SC2ZRc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -243,6 +264,7 @@ github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@@ -260,10 +282,12 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -296,6 +320,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY=
github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ=
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -333,6 +358,7 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@@ -396,6 +422,7 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -411,6 +438,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -465,12 +493,12 @@ github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/buildkit v0.11.0-rc3.0.20230609092854-67a08623b95a h1:1k3bAXwxC2N1FncWijq/43sLj2OVIZ11FT0APIXWhMg=
github.com/moby/buildkit v0.11.0-rc3.0.20230609092854-67a08623b95a/go.mod h1:4sM7BBBqXOQ+vV6LrVAOAMhZI9cVNYV5RhZCl906a64=
github.com/moby/buildkit v0.12.1 h1:vvMG7EZYCiQZpTtXQkvyeyj7HzT1JHhDWj+/aiGIzLM=
github.com/moby/buildkit v0.12.1/go.mod h1:adB4y0SxxX8trnrY+oEulb48ODLqPO6pKMF0ppGcCoI=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
@@ -502,21 +530,25 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs=
github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys=
github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk=
github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
github.com/opencontainers/runtime-spec v1.1.0-rc.2 h1:ucBtEms2tamYYW/SvGpvq9yUN0NEVL6oyLEwDcTSrk8=
github.com/opencontainers/runtime-spec v1.1.0-rc.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
@@ -557,7 +589,6 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
@@ -579,7 +610,6 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -588,6 +618,7 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spdx/tools-golang v0.5.1 h1:fJg3SVOGG+eIva9ZUBm/hvyA7PIPVFjRxUKe6fdAgwE=
github.com/spdx/tools-golang v0.5.1/go.mod h1:/DRDQuBfB37HctM29YtrX1v+bXiVmT2OpQDalRmX9aU=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
@@ -605,6 +636,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -626,13 +658,14 @@ github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4D
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g=
github.com/tonistiigi/fsutil v0.0.0-20230407161946-9e7a6df48576 h1:fZXPQDVh5fm2x7pA0CH1TtH80tiZ0L7i834kZqZN8Pw=
github.com/tonistiigi/fsutil v0.0.0-20230407161946-9e7a6df48576/go.mod h1:q1CxMSzcAbjUkVGHoZeQUcCaALnaE4XdWk+zJcgMYFw=
github.com/tonistiigi/fsutil v0.0.0-20230629203738-36ef4d8c0dbb h1:uUe8rNyVXM8moActoBol6Xf6xX2GMr7SosR2EywMvGg=
github.com/tonistiigi/fsutil v0.0.0-20230629203738-36ef4d8c0dbb/go.mod h1:SxX/oNQ/ag6Vaoli547ipFK9J7BZn5JqJG0JE8lf8bA=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f h1:DLpt6B5oaaS8jyXHa9VA4rrZloBVPVXeCtrOsrFauxc=
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 h1:Y/M5lygoNPKwVNLMPXgVfsRT40CSFKXCxuU8LoHySjs=
github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
github.com/weppos/publicsuffix-go v0.15.1-0.20220329081811-9a40b608a236 h1:vMJBP3PQViZsF6cOINtvyMC8ptpLsyJ4EwyFnzuWNxc=
github.com/weppos/publicsuffix-go v0.15.1-0.20220329081811-9a40b608a236/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -653,6 +686,7 @@ github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54t
github.com/zmap/zcrypto v0.0.0-20220605182715-4dfcec6e9a8c h1:ufDm/IlBYZYLuiqvQuhpTKwrcAS2OlXEzWbDvTVGbSQ=
github.com/zmap/zcrypto v0.0.0-20220605182715-4dfcec6e9a8c/go.mod h1:egdRkzUylATvPkWMpebZbXhv0FMEMJGX/ur0D3Csk2s=
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -661,6 +695,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 h1:5jD3teb4Qh7mx/nfzq4jO2WFFpvXD0vYWFDrdvNWmXk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0/go.mod h1:UMklln0+MRhZC4e3PwmN3pCtq4DyIadWw4yikh6bNrw=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.40.0 h1:ZjF6qLnAVNq6xUh0sK2mCEqwnRrpgr0mLALQXJL34NI=
@@ -716,6 +751,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -741,8 +778,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -863,7 +900,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -1015,8 +1051,12 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M=
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk=
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ=
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -1039,8 +1079,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE=
google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1088,8 +1128,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -0,0 +1,41 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package locker
import (
"fmt"
"os"
"github.com/adrg/xdg"
"github.com/docker/docker/pkg/pidfile"
)
type Pidfile struct {
path string
}
func NewPidfile(projectName string) (*Pidfile, error) {
path, err := xdg.RuntimeFile(fmt.Sprintf("docker-compose.%s.pid", projectName))
if err != nil {
return nil, err
}
return &Pidfile{path: path}, nil
}
func (f *Pidfile) Lock() error {
return pidfile.Write(f.path, os.Getpid())
}

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

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

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

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

354
internal/sync/tar.go Normal file
View File

@@ -0,0 +1,354 @@
/*
Copyright 2018 The Tilt Dev 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sync
import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
)
type archiveEntry struct {
path string
info os.FileInfo
header *tar.Header
}
type LowLevelClient interface {
ContainersForService(ctx context.Context, projectName string, serviceName string) ([]moby.Container, error)
Exec(ctx context.Context, containerID string, cmd []string, in io.Reader) error
}
type Tar struct {
client LowLevelClient
projectName string
}
var _ Syncer = &Tar{}
func NewTar(projectName string, client LowLevelClient) *Tar {
return &Tar{
projectName: projectName,
client: client,
}
}
func (t *Tar) Sync(ctx context.Context, service types.ServiceConfig, paths []PathMapping) error {
containers, err := t.client.ContainersForService(ctx, t.projectName, service.Name)
if err != nil {
return err
}
var pathsToCopy []PathMapping
var pathsToDelete []string
for _, p := range paths {
if _, err := os.Stat(p.HostPath); err != nil && errors.Is(err, fs.ErrNotExist) {
pathsToDelete = append(pathsToDelete, p.ContainerPath)
} else {
pathsToCopy = append(pathsToCopy, p)
}
}
var deleteCmd []string
if len(pathsToDelete) != 0 {
deleteCmd = append([]string{"rm", "-rf"}, pathsToDelete...)
}
copyCmd := []string{"tar", "-v", "-C", "/", "-x", "-f", "-"}
var eg multierror.Group
writers := make([]*io.PipeWriter, len(containers))
for i := range containers {
containerID := containers[i].ID
r, w := io.Pipe()
writers[i] = w
eg.Go(func() error {
if len(deleteCmd) != 0 {
if err := t.client.Exec(ctx, containerID, deleteCmd, nil); err != nil {
return fmt.Errorf("deleting paths in %s: %w", containerID, err)
}
}
if err := t.client.Exec(ctx, containerID, copyCmd, r); err != nil {
return fmt.Errorf("copying files to %s: %w", containerID, err)
}
return nil
})
}
multiWriter := newLossyMultiWriter(writers...)
tarReader := tarArchive(pathsToCopy)
defer func() {
_ = tarReader.Close()
multiWriter.Close()
}()
_, err = io.Copy(multiWriter, tarReader)
if err != nil {
return err
}
multiWriter.Close()
return eg.Wait().ErrorOrNil()
}
type ArchiveBuilder struct {
tw *tar.Writer
// A shared I/O buffer to help with file copying.
copyBuf *bytes.Buffer
}
func NewArchiveBuilder(writer io.Writer) *ArchiveBuilder {
tw := tar.NewWriter(writer)
return &ArchiveBuilder{
tw: tw,
copyBuf: &bytes.Buffer{},
}
}
func (a *ArchiveBuilder) Close() error {
return a.tw.Close()
}
// ArchivePathsIfExist creates a tar archive of all local files in `paths`. It quietly skips any paths that don't exist.
func (a *ArchiveBuilder) ArchivePathsIfExist(paths []PathMapping) error {
// In order to handle overlapping syncs, we
// 1) collect all the entries,
// 2) de-dupe them, with last-one-wins semantics
// 3) write all the entries
//
// It's not obvious that this is the correct behavior. A better approach
// (that's more in-line with how syncs work) might ignore files in earlier
// path mappings when we know they're going to be "synced" over.
// There's a bunch of subtle product decisions about how overlapping path
// mappings work that we're not sure about.
var entries []archiveEntry
for _, p := range paths {
newEntries, err := a.entriesForPath(p.HostPath, p.ContainerPath)
if err != nil {
return fmt.Errorf("inspecting %q: %w", p.HostPath, err)
}
entries = append(entries, newEntries...)
}
entries = dedupeEntries(entries)
for _, entry := range entries {
err := a.writeEntry(entry)
if err != nil {
return fmt.Errorf("archiving %q: %w", entry.path, err)
}
}
return nil
}
func (a *ArchiveBuilder) writeEntry(entry archiveEntry) error {
pathInTar := entry.path
header := entry.header
if header.Typeflag != tar.TypeReg {
// anything other than a regular file (e.g. dir, symlink) just needs the header
if err := a.tw.WriteHeader(header); err != nil {
return fmt.Errorf("writing %q header: %w", pathInTar, err)
}
return nil
}
file, err := os.Open(pathInTar)
if err != nil {
// In case the file has been deleted since we last looked at it.
if os.IsNotExist(err) {
return nil
}
return err
}
defer func() {
_ = file.Close()
}()
// The size header must match the number of contents bytes.
//
// There is room for a race condition here if something writes to the file
// after we've read the file size.
//
// For small files, we avoid this by first copying the file into a buffer,
// and using the size of the buffer to populate the header.
//
// For larger files, we don't want to copy the whole thing into a buffer,
// because that would blow up heap size. There is some danger that this
// will lead to a spurious error when the tar writer validates the sizes.
// That error will be disruptive but will be handled as best as we
// can downstream.
useBuf := header.Size < 5000000
if useBuf {
a.copyBuf.Reset()
_, err = io.Copy(a.copyBuf, file)
if err != nil && err != io.EOF {
return fmt.Errorf("copying %q: %w", pathInTar, err)
}
header.Size = int64(len(a.copyBuf.Bytes()))
}
// wait to write the header until _after_ the file is successfully opened
// to avoid generating an invalid tar entry that has a header but no contents
// in the case the file has been deleted
err = a.tw.WriteHeader(header)
if err != nil {
return fmt.Errorf("writing %q header: %w", pathInTar, err)
}
if useBuf {
_, err = io.Copy(a.tw, a.copyBuf)
} else {
_, err = io.Copy(a.tw, file)
}
if err != nil && err != io.EOF {
return fmt.Errorf("copying %q: %w", pathInTar, err)
}
// explicitly flush so that if the entry is invalid we will detect it now and
// provide a more meaningful error
if err := a.tw.Flush(); err != nil {
return fmt.Errorf("finalizing %q: %w", pathInTar, err)
}
return nil
}
// tarPath writes the given source path into tarWriter at the given dest (recursively for directories).
// e.g. tarring my_dir --> dest d: d/file_a, d/file_b
// If source path does not exist, quietly skips it and returns no err
func (a *ArchiveBuilder) entriesForPath(localPath, containerPath string) ([]archiveEntry, error) {
localInfo, err := os.Stat(localPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
localPathIsDir := localInfo.IsDir()
if localPathIsDir {
// Make sure we can trim this off filenames to get valid relative filepaths
if !strings.HasSuffix(localPath, string(filepath.Separator)) {
localPath += string(filepath.Separator)
}
}
containerPath = strings.TrimPrefix(containerPath, "/")
result := make([]archiveEntry, 0)
err = filepath.Walk(localPath, func(curLocalPath string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("walking %q: %w", curLocalPath, err)
}
linkname := ""
if info.Mode()&os.ModeSymlink != 0 {
var err error
linkname, err = os.Readlink(curLocalPath)
if err != nil {
return err
}
}
var name string
//nolint:gocritic
if localPathIsDir {
// Name of file in tar should be relative to source directory...
tmp, err := filepath.Rel(localPath, curLocalPath)
if err != nil {
return fmt.Errorf("making %q relative to %q: %w", curLocalPath, localPath, err)
}
// ...and live inside `dest`
name = path.Join(containerPath, filepath.ToSlash(tmp))
} else if strings.HasSuffix(containerPath, "/") {
name = containerPath + filepath.Base(curLocalPath)
} else {
name = containerPath
}
header, err := archive.FileInfoHeader(name, info, linkname)
if err != nil {
// Not all types of files are allowed in a tarball. That's OK.
// Mimic the Docker behavior and just skip the file.
return nil
}
result = append(result, archiveEntry{
path: curLocalPath,
info: info,
header: header,
})
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func tarArchive(ops []PathMapping) io.ReadCloser {
pr, pw := io.Pipe()
go func() {
ab := NewArchiveBuilder(pw)
err := ab.ArchivePathsIfExist(ops)
if err != nil {
_ = pw.CloseWithError(fmt.Errorf("adding files to tar: %w", err))
} else {
// propagate errors from the TarWriter::Close() because it performs a final
// Flush() and any errors mean the tar is invalid
if err := ab.Close(); err != nil {
_ = pw.CloseWithError(fmt.Errorf("closing tar: %w", err))
} else {
_ = pw.Close()
}
}
}()
return pr
}
// Dedupe the entries with last-entry-wins semantics.
func dedupeEntries(entries []archiveEntry) []archiveEntry {
seenIndex := make(map[string]int, len(entries))
result := make([]archiveEntry, 0, len(entries))
for i, entry := range entries {
seenIndex[entry.header.Name] = i
}
for i, entry := range entries {
if seenIndex[entry.header.Name] == i {
result = append(result, entry)
}
}
return result
}

91
internal/sync/writer.go Normal file
View File

@@ -0,0 +1,91 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sync
import (
"errors"
"io"
)
// lossyMultiWriter attempts to tee all writes to the provided io.PipeWriter
// instances.
//
// If a writer fails during a Write call, the write-side of the pipe is then
// closed with the error and no subsequent attempts are made to write to the
// pipe.
//
// If all writers fail during a write, an error is returned.
//
// On Close, any remaining writers are closed.
type lossyMultiWriter struct {
writers []*io.PipeWriter
}
// newLossyMultiWriter creates a new writer that *attempts* to tee all data written to it to the provided io.PipeWriter
// instances. Rather than failing a write operation if any writer fails, writes only fail if there are no more valid
// writers. Otherwise, errors for specific writers are propagated via CloseWithError.
func newLossyMultiWriter(writers ...*io.PipeWriter) *lossyMultiWriter {
// reverse the writers because during the write we iterate
// backwards, so this way we'll end up writing in the same
// order as the writers were passed to us
writers = append([]*io.PipeWriter(nil), writers...)
for i, j := 0, len(writers)-1; i < j; i, j = i+1, j-1 {
writers[i], writers[j] = writers[j], writers[i]
}
return &lossyMultiWriter{
writers: writers,
}
}
// Write writes to each writer that is still active (i.e. has not failed/encountered an error on write).
//
// If a writer encounters an error during the write, the write side of the pipe is closed with the error
// and no subsequent attempts will be made to write to that writer.
//
// An error is only returned from this function if ALL writers have failed.
func (l *lossyMultiWriter) Write(p []byte) (int, error) {
// NOTE: this function iterates backwards so that it can
// safely remove elements during the loop
for i := len(l.writers) - 1; i >= 0; i-- {
written, err := l.writers[i].Write(p)
if err == nil && written != len(p) {
err = io.ErrShortWrite
}
if err != nil {
// pipe writer close cannot fail
_ = l.writers[i].CloseWithError(err)
l.writers = append(l.writers[:i], l.writers[i+1:]...)
}
}
if len(l.writers) == 0 {
return 0, errors.New("no writers remaining")
}
return len(p), nil
}
// Close closes any still open (non-failed) writers.
//
// Failed writers have already been closed with an error.
func (l *lossyMultiWriter) Close() {
for i := range l.writers {
// pipe writer close cannot fail
_ = l.writers[i].Close()
}
}

View File

@@ -0,0 +1,152 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sync
import (
"context"
"io"
"sync"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
func TestLossyMultiWriter(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
const count = 5
readers := make([]*bufReader, count)
writers := make([]*io.PipeWriter, count)
for i := 0; i < count; i++ {
r, w := io.Pipe()
readers[i] = newBufReader(ctx, r)
writers[i] = w
}
w := newLossyMultiWriter(writers...)
t.Cleanup(w.Close)
n, err := w.Write([]byte("hello world"))
require.Equal(t, 11, n)
require.NoError(t, err)
for i := range readers {
readers[i].waitForWrite(t)
require.Equal(t, "hello world", string(readers[i].contents()))
readers[i].reset()
}
// even if a writer fails (in this case simulated by closing the receiving end of the pipe),
// write operations should continue to return nil error but the writer should be closed
// with an error
const failIndex = 3
require.NoError(t, readers[failIndex].r.CloseWithError(errors.New("oh no")))
n, err = w.Write([]byte("hello"))
require.Equal(t, 5, n)
require.NoError(t, err)
for i := range readers {
readers[i].waitForWrite(t)
if i == failIndex {
err := readers[i].error()
require.EqualError(t, err, "io: read/write on closed pipe")
require.Empty(t, readers[i].contents())
} else {
require.Equal(t, "hello", string(readers[i].contents()))
}
}
// perform another write, verify there's still no errors
n, err = w.Write([]byte(" world"))
require.Equal(t, 6, n)
require.NoError(t, err)
}
type bufReader struct {
ctx context.Context
r *io.PipeReader
mu sync.Mutex
err error
data []byte
writeSync chan struct{}
}
func newBufReader(ctx context.Context, r *io.PipeReader) *bufReader {
b := &bufReader{
ctx: ctx,
r: r,
writeSync: make(chan struct{}),
}
go b.consume()
return b
}
func (b *bufReader) waitForWrite(t testing.TB) {
t.Helper()
select {
case <-b.writeSync:
return
case <-time.After(50 * time.Millisecond):
t.Fatal("timed out waiting for write")
}
}
func (b *bufReader) consume() {
defer close(b.writeSync)
for {
buf := make([]byte, 512)
n, err := b.r.Read(buf)
if n != 0 {
b.mu.Lock()
b.data = append(b.data, buf[:n]...)
b.mu.Unlock()
}
if err == io.EOF {
return
}
if err != nil {
b.mu.Lock()
b.err = err
b.mu.Unlock()
return
}
// prevent goroutine leak, tie lifetime to the test
select {
case b.writeSync <- struct{}{}:
case <-b.ctx.Done():
return
}
}
}
func (b *bufReader) contents() []byte {
b.mu.Lock()
defer b.mu.Unlock()
return b.data
}
func (b *bufReader) reset() {
b.mu.Lock()
defer b.mu.Unlock()
b.data = nil
}
func (b *bufReader) error() error {
b.mu.Lock()
defer b.mu.Unlock()
return b.err
}

View File

@@ -0,0 +1,165 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tracing
import (
"strings"
"time"
"github.com/docker/compose/v2/pkg/utils"
"github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// SpanOptions is a small helper type to make it easy to share the options helpers between
// downstream functions that accept slices of trace.SpanStartOption and trace.EventOption.
type SpanOptions []trace.SpanStartEventOption
func (s SpanOptions) SpanStartOptions() []trace.SpanStartOption {
out := make([]trace.SpanStartOption, len(s))
for i := range s {
out[i] = s[i]
}
return out
}
func (s SpanOptions) EventOptions() []trace.EventOption {
out := make([]trace.EventOption, len(s))
for i := range s {
out[i] = s[i]
}
return out
}
// ProjectOptions returns common attributes from a Compose project.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ProjectOptions(proj *types.Project) SpanOptions {
if proj == nil {
return nil
}
disabledServiceNames := make([]string, len(proj.DisabledServices))
for i := range proj.DisabledServices {
disabledServiceNames[i] = proj.DisabledServices[i].Name
}
attrs := []attribute.KeyValue{
attribute.String("project.name", proj.Name),
attribute.String("project.dir", proj.WorkingDir),
attribute.StringSlice("project.compose_files", proj.ComposeFiles),
attribute.StringSlice("project.services.active", proj.ServiceNames()),
attribute.StringSlice("project.services.disabled", disabledServiceNames),
attribute.StringSlice("project.profiles", proj.Profiles),
attribute.StringSlice("project.volumes", proj.VolumeNames()),
attribute.StringSlice("project.networks", proj.NetworkNames()),
attribute.StringSlice("project.secrets", proj.SecretNames()),
attribute.StringSlice("project.configs", proj.ConfigNames()),
attribute.StringSlice("project.extensions", keys(proj.Extensions)),
attribute.StringSlice("project.includes", flattenIncludeReferences(proj.IncludeReferences)),
}
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
// ServiceOptions returns common attributes from a Compose service.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ServiceOptions(service types.ServiceConfig) SpanOptions {
attrs := []attribute.KeyValue{
attribute.String("service.name", service.Name),
attribute.String("service.image", service.Image),
attribute.StringSlice("service.networks", keys(service.Networks)),
}
configNames := make([]string, len(service.Configs))
for i := range service.Configs {
configNames[i] = service.Configs[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.configs", configNames))
secretNames := make([]string, len(service.Secrets))
for i := range service.Secrets {
secretNames[i] = service.Secrets[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.secrets", secretNames))
volNames := make([]string, len(service.Volumes))
for i := range service.Volumes {
volNames[i] = service.Volumes[i].Source
}
attrs = append(attrs, attribute.StringSlice("service.volumes", volNames))
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
// ContainerOptions returns common attributes from a Moby container.
//
// For convenience, it's returned as a SpanOptions object to allow it to be
// passed directly to the wrapping helper methods in this package such as
// SpanWrapFunc.
func ContainerOptions(container moby.Container) SpanOptions {
attrs := []attribute.KeyValue{
attribute.String("container.id", container.ID),
attribute.String("container.image", container.Image),
unixTimeAttr("container.created_at", container.Created),
}
if len(container.Names) != 0 {
attrs = append(attrs, attribute.String("container.name", strings.TrimPrefix(container.Names[0], "/")))
}
return []trace.SpanStartEventOption{
trace.WithAttributes(attrs...),
}
}
func keys[T any](m map[string]T) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func timeAttr(key string, value time.Time) attribute.KeyValue {
return attribute.String(key, value.Format(time.RFC3339))
}
func unixTimeAttr(key string, value int64) attribute.KeyValue {
return timeAttr(key, time.Unix(value, 0).UTC())
}
func flattenIncludeReferences(includeRefs map[string][]types.IncludeConfig) []string {
ret := utils.NewSet[string]()
for _, included := range includeRefs {
for i := range included {
ret.AddAll(included[i].Path...)
}
}
return ret.Elements()
}

View File

@@ -69,7 +69,6 @@ func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptr
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)

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

@@ -0,0 +1,91 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tracing
import (
"context"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
"go.opentelemetry.io/otel/trace"
)
// SpanWrapFunc wraps a function that takes a context with a trace.Span, marking the status as codes.Error if the
// wrapped function returns an error.
//
// The context passed to the function is created from the span to ensure correct propagation.
//
// NOTE: This function is nearly identical to SpanWrapFuncForErrGroup, except the latter is designed specially for
// convenience with errgroup.Group due to its prevalence throughout the codebase. The code is duplicated to avoid
// adding even more levels of function wrapping/indirection.
func SpanWrapFunc(spanName string, opts SpanOptions, fn func(ctx context.Context) error) func(context.Context) error {
return func(ctx context.Context) error {
ctx, span := Tracer.Start(ctx, spanName, opts.SpanStartOptions()...)
defer span.End()
if err := fn(ctx); err != nil {
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
}
}
// SpanWrapFuncForErrGroup wraps a function that takes a context with a trace.Span, marking the status as codes.Error
// if the wrapped function returns an error.
//
// The context passed to the function is created from the span to ensure correct propagation.
//
// NOTE: This function is nearly identical to SpanWrapFunc, except this function is designed specially for
// convenience with errgroup.Group due to its prevalence throughout the codebase. The code is duplicated to avoid
// adding even more levels of function wrapping/indirection.
func SpanWrapFuncForErrGroup(ctx context.Context, spanName string, opts SpanOptions, fn func(ctx context.Context) error) func() error {
return func() error {
ctx, span := Tracer.Start(ctx, spanName, opts.SpanStartOptions()...)
defer span.End()
if err := fn(ctx); err != nil {
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
}
}
// EventWrapFuncForErrGroup invokes a function and records an event, optionally including the returned
// error as the "exception message" on the event.
//
// This is intended for lightweight usage to wrap errgroup.Group calls where a full span is not desired.
func EventWrapFuncForErrGroup(ctx context.Context, eventName string, opts SpanOptions, fn func(ctx context.Context) error) func() error {
return func() error {
span := trace.SpanFromContext(ctx)
eventOpts := opts.EventOptions()
err := fn(ctx)
if err != nil {
eventOpts = append(eventOpts, trace.WithAttributes(semconv.ExceptionMessage(err.Error())))
}
span.AddEvent(eventName, eventOpts...)
return err
}
}

View File

@@ -52,7 +52,7 @@ type Service interface {
Ps(ctx context.Context, projectName string, options PsOptions) ([]ContainerSummary, error)
// List executes the equivalent to a `docker stack ls`
List(ctx context.Context, options ListOptions) ([]Stack, error)
// Convert translate compose model into backend's native format
// Config executes the equivalent to a `compose config`
Config(ctx context.Context, project *types.Project, options ConfigOptions) ([]byte, error)
// Kill executes the equivalent to a `compose kill`
Kill(ctx context.Context, projectName string, options KillOptions) error
@@ -145,7 +145,6 @@ func (o BuildOptions) Apply(project *types.Project) error {
if service.Build == nil {
continue
}
service.Image = GetImageNameOrDefault(service, project.Name)
if platform != "" {
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, platform) {
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", service.Name, platform)
@@ -391,18 +390,25 @@ type PortPublisher struct {
// ContainerSummary hold high-level description of a container
type ContainerSummary struct {
ID string
Name string
Image any
Command string
Project string
Service string
Created int64
State string
Status string
Health string
ExitCode int
Publishers PortPublishers
ID string
Name string
Names []string
Image string
Command string
Project string
Service string
Created int64
State string
Status string
Health string
ExitCode int
Publishers PortPublishers
Labels map[string]string
SizeRw int64 `json:",omitempty"`
SizeRootFs int64 `json:",omitempty"`
Mounts []string
Networks []string
LocalVolumes int
}
// PortPublishers is a slice of PortPublisher

View File

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

View File

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

View File

@@ -98,7 +98,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
return err
}
func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, stdin io.ReadCloser, stdout, stderr io.Writer) (func(), chan bool, error) {
func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, stdin io.ReadCloser, stdout, stderr io.WriteCloser) (func(), chan bool, error) {
detached := make(chan bool)
var (
restore = func() { /* noop */ }
@@ -140,6 +140,8 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container s
if stdout != nil {
go func() {
defer stdout.Close() //nolint:errcheck
defer stderr.Close() //nolint:errcheck
if tty {
io.Copy(stdout, streamOut) //nolint:errcheck
} else {

View File

@@ -22,15 +22,19 @@ 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"
_ "github.com/docker/buildx/driver/docker" // required to get default driver registered
"github.com/docker/buildx/builder"
"github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/store/storeutil"
"github.com/docker/buildx/util/buildflags"
xprogress "github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/builder/remotecontext/urlutil"
bclient "github.com/moby/buildkit/client"
"github.com/moby/buildkit/session"
@@ -42,9 +46,8 @@ import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
// required to get default driver registered
_ "github.com/docker/buildx/driver/docker"
)
func (s *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
@@ -58,26 +61,52 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
}, s.stdinfo(), "Building")
}
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
args := options.Args.Resolve(envResolver(project.Environment))
//nolint:gocyclo
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) {
buildkitEnabled, err := s.dockerCli.BuildKitEnabled()
if err != nil {
return nil, err
}
// Progress needs its own context that lives longer than the
// build one otherwise it won't read all the messages from
// build and will lock
progressCtx, cancel := context.WithCancel(context.Background())
defer cancel()
// Initialize buildkit nodes
var (
b *builder.Builder
nodes []builder.Node
w *xprogress.Printer
)
if buildkitEnabled {
builderName := options.Builder
if builderName == "" {
builderName = os.Getenv("BUILDX_BUILDER")
}
b, err = builder.New(s.dockerCli, builder.WithName(builderName))
if err != nil {
return nil, err
}
w, err := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, options.Progress)
if err != nil {
return nil, err
nodes, err = b.LoadNodes(ctx, false)
if err != nil {
return nil, err
}
// Progress needs its own context that lives longer than the
// build one otherwise it won't read all the messages from
// build and will lock
progressCtx, cancel := context.WithCancel(context.Background())
defer cancel()
w, err = xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, options.Progress,
xprogress.WithDesc(
fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver),
fmt.Sprintf("%s:%s", b.Driver, b.Name),
))
if err != nil {
return nil, err
}
}
builtIDs := make([]string, len(project.Services))
builtDigests := make([]string, len(project.Services))
err = InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
return nil
@@ -89,16 +118,11 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
}
if !buildkitEnabled {
if service.Build.Args == nil {
service.Build.Args = args
} else {
service.Build.Args = service.Build.Args.OverrideBy(args)
}
id, err := s.doBuildClassic(ctx, service, options)
id, err := s.doBuildClassic(ctx, project, service, options)
if err != nil {
return err
}
builtIDs[idx] = id
builtDigests[idx] = id
if options.Push {
return s.push(ctx, project, api.PushOptions{})
@@ -114,13 +138,12 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
if err != nil {
return err
}
buildOptions.BuildArgs = mergeArgs(buildOptions.BuildArgs, flatten(args))
digest, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w, options.Builder)
digest, err := s.doBuildBuildkit(ctx, service.Name, buildOptions, w, nodes)
if err != nil {
return err
}
builtIDs[idx] = digest
builtDigests[idx] = digest
return nil
}, func(traversal *graphTraversal) {
@@ -128,8 +151,10 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
})
// enforce all build event get consumed
if errw := w.Wait(); errw != nil {
return nil, errw
if buildkitEnabled {
if errw := w.Wait(); errw != nil {
return nil, errw
}
}
if err != nil {
@@ -137,9 +162,10 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
}
imageIDs := map[string]string{}
for i, d := range builtIDs {
if d != "" {
imageIDs[project.Services[i].Image] = d
for i, imageDigest := range builtDigests {
if imageDigest != "" {
imageRef := api.GetImageNameOrDefault(project.Services[i], project.Name)
imageIDs[imageRef] = imageDigest
}
}
return imageIDs, err
@@ -169,7 +195,11 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
return err
}
err = s.pullRequiredImages(ctx, project, images, quietPull)
err = tracing.SpanWrapFunc("project/pull", tracing.ProjectOptions(project),
func(ctx context.Context) error {
return s.pullRequiredImages(ctx, project, images, quietPull)
},
)(ctx)
if err != nil {
return err
}
@@ -185,16 +215,24 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
}
if buildRequired {
builtImages, err := s.build(ctx, project, api.BuildOptions{
Progress: mode,
})
err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(project),
func(ctx context.Context) error {
builtImages, err := s.build(ctx, project, api.BuildOptions{
Progress: mode,
})
if err != nil {
return err
}
for name, digest := range builtImages {
images[name] = digest
}
return nil
},
)(ctx)
if err != nil {
return err
}
for name, digest := range builtImages {
images[name] = digest
}
}
// set digest as com.docker.compose.image label so we can detect outdated containers
@@ -222,7 +260,8 @@ func (s *composeService) prepareProjectForBuild(project *types.Project, images m
continue
}
_, localImagePresent := images[service.Image]
image := api.GetImageNameOrDefault(service, project.Name)
_, localImagePresent := images[image]
if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
service.Build = nil
project.Services[i] = service
@@ -291,17 +330,38 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
return images, nil
}
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
tags := []string{service.Image}
// resolveAndMergeBuildArgs returns the final set of build arguments to use for the service image build.
//
// First, args directly defined via `build.args` in YAML are considered.
// Then, any explicitly passed args in opts (e.g. via `--build-arg` on the CLI) are merged, overwriting any
// keys that already exist.
// Next, any keys without a value are resolved using the project environment.
//
// Finally, standard proxy variables based on the Docker client configuration are added, but will not overwrite
// any values if already present.
func resolveAndMergeBuildArgs(
dockerCli command.Cli,
project *types.Project,
service types.ServiceConfig,
opts api.BuildOptions,
) types.MappingWithEquals {
result := make(types.MappingWithEquals).
OverrideBy(service.Build.Args).
OverrideBy(opts.Args).
Resolve(envResolver(project.Environment))
buildArgs := flatten(service.Build.Args.Resolve(envResolver(project.Environment)))
for k, v := range storeutil.GetProxyConfig(s.dockerCli) {
if _, ok := buildArgs[k]; !ok {
buildArgs[k] = v
// proxy arguments do NOT override and should NOT have env resolution applied,
// so they're handled last
for k, v := range storeutil.GetProxyConfig(dockerCli) {
if _, ok := result[k]; !ok {
v := v
result[k] = &v
}
}
return result
}
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
plats, err := addPlatforms(project, service)
if err != nil {
return build.Options{}, err
@@ -335,6 +395,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
sessionConfig = append(sessionConfig, secretsProvider)
}
tags := []string{api.GetImageNameOrDefault(service, project.Name)}
if len(service.Build.Tags) > 0 {
tags = append(tags, service.Build.Tags...)
}
@@ -345,18 +406,19 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
imageLabels := getImageBuildLabels(project, service)
push := options.Push && service.Image != ""
exports := []bclient.ExportEntry{{
Type: "docker",
Attrs: map[string]string{
"load": "true",
"push": fmt.Sprint(options.Push),
"push": fmt.Sprint(push),
},
}}
if len(service.Build.Platforms) > 1 {
exports = []bclient.ExportEntry{{
Type: "image",
Attrs: map[string]string{
"push": fmt.Sprint(options.Push),
"push": fmt.Sprint(push),
},
}}
}
@@ -372,7 +434,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
CacheTo: pb.CreateCaches(cacheTo),
NoCache: service.Build.NoCache,
Pull: service.Build.Pull,
BuildArgs: buildArgs,
BuildArgs: flatten(resolveAndMergeBuildArgs(s.dockerCli, project, service, options)),
Tags: tags,
Target: service.Build.Target,
Exports: exports,
@@ -399,16 +461,6 @@ func flatten(in types.MappingWithEquals) types.Mapping {
return out
}
func mergeArgs(m ...types.Mapping) types.Mapping {
merged := types.Mapping{}
for _, mapping := range m {
for key, val := range mapping {
merged[key] = val
}
}
return merged
}
func dockerFilePath(ctxName string, dockerfile string) string {
if dockerfile == "" {
return ""

View File

@@ -34,18 +34,11 @@ import (
"github.com/moby/buildkit/client"
)
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 "", err
}
nodes, err := b.LoadNodes(ctx, false)
if err != nil {
return "", err
}
var response map[string]*client.SolveResponse
func (s *composeService) doBuildBuildkit(ctx context.Context, service string, opts build.Options, p *buildx.Printer, nodes []builder.Node) (string, error) {
var (
response map[string]*client.SolveResponse
err error
)
if s.dryRun {
response = s.dryRunBuildResponse(ctx, service, opts)
} else {

View File

@@ -26,6 +26,8 @@ import (
"runtime"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/registry"
"github.com/compose-spec/compose-go/types"
@@ -45,7 +47,7 @@ import (
)
//nolint:gocyclo
func (s *composeService) doBuildClassic(ctx context.Context, service types.ServiceConfig, options api.BuildOptions) (string, error) {
func (s *composeService) doBuildClassic(ctx context.Context, project *types.Project, service types.ServiceConfig, options api.BuildOptions) (string, error) {
var (
buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser
@@ -159,8 +161,9 @@ func (s *composeService) doBuildClassic(ctx context.Context, service types.Servi
for k, auth := range creds {
authConfigs[k] = registry.AuthConfig(auth)
}
buildOptions := imageBuildOptions(service.Build)
buildOptions.Tags = append(buildOptions.Tags, service.Image)
buildOptions := imageBuildOptions(s.dockerCli, project, service, options)
imageName := api.GetImageNameOrDefault(service, project.Name)
buildOptions.Tags = append(buildOptions.Tags, imageName)
buildOptions.Dockerfile = relDockerfile
buildOptions.AuthConfigs = authConfigs
buildOptions.Memory = options.Memory
@@ -214,14 +217,15 @@ func isLocalDir(c string) bool {
return err == nil
}
func imageBuildOptions(config *types.BuildConfig) dockertypes.ImageBuildOptions {
func imageBuildOptions(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, options api.BuildOptions) dockertypes.ImageBuildOptions {
config := service.Build
return dockertypes.ImageBuildOptions{
Version: dockertypes.BuilderV1,
Tags: config.Tags,
NoCache: config.NoCache,
Remove: true,
PullParent: config.Pull,
BuildArgs: config.Args,
BuildArgs: resolveAndMergeBuildArgs(dockerCli, project, service, options),
Labels: config.Labels,
NetworkMode: config.Network,
ExtraHosts: config.ExtraHosts.AsList(),

View File

@@ -26,6 +26,8 @@ import (
"strings"
"sync"
"github.com/jonboulle/clockwork"
"github.com/docker/docker/api/types/volume"
"github.com/compose-spec/compose-go/types"
@@ -58,6 +60,7 @@ func init() {
func NewComposeService(dockerCli command.Cli) api.Service {
return &composeService{
dockerCli: dockerCli,
clock: clockwork.NewRealClock(),
maxConcurrency: -1,
dryRun: false,
}
@@ -65,6 +68,7 @@ func NewComposeService(dockerCli command.Cli) api.Service {
type composeService struct {
dockerCli command.Cli
clock clockwork.Clock
maxConcurrency int
dryRun bool
}
@@ -88,8 +92,11 @@ func (s *composeService) DryRunMode(ctx context.Context, dryRun bool) (context.C
if err != nil {
return ctx, err
}
err = cli.Initialize(flags.NewClientOptions(), command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
return api.NewDryRunClient(s.apiClient(), cli)
options := flags.NewClientOptions()
options.Context = s.dockerCli.CurrentContext()
err = cli.Initialize(options, command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
return api.NewDryRunClient(s.apiClient(), s.dockerCli)
}))
if err != nil {
return ctx, err
@@ -200,6 +207,7 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
condition := ServiceConditionRunningOrHealthy
// Let's restart the dependency by default if we don't have the info stored in the label
restart := true
required := true
dependency := dcArr[0]
// backward compatibility
@@ -209,7 +217,7 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
restart, _ = strconv.ParseBool(dcArr[2])
}
}
service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart}
service.DependsOn[dependency] = types.ServiceDependency{Condition: condition, Restart: restart, Required: required}
}
}
project.Services = append(project.Services, *service)

View File

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

View File

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

View File

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

View File

@@ -139,6 +139,7 @@ func prepareVolumes(p *types.Project) error {
p.Services[i].DependsOn[service.Name].Condition == "" {
p.Services[i].DependsOn[service.Name] = types.ServiceDependency{
Condition: types.ServiceConditionStarted,
Required: true,
}
}
}
@@ -862,10 +863,14 @@ func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount
target := config.Target
if config.Target == "" {
target = configsBaseDir + config.Source
} else if !isUnixAbs(config.Target) {
} else if !isAbsTarget(config.Target) {
target = configsBaseDir + config.Target
}
if config.UID != "" || config.GID != "" || config.Mode != nil {
logrus.Warn("config `uid`, `gid` and `mode` are not supported, they will be ignored")
}
definedConfig := p.Configs[config.Source]
if definedConfig.External.External {
return nil, fmt.Errorf("unsupported external config %s", definedConfig.Name)
@@ -897,10 +902,14 @@ func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount
target := secret.Target
if secret.Target == "" {
target = secretsDir + secret.Source
} else if !isUnixAbs(secret.Target) {
} else if !isAbsTarget(secret.Target) {
target = secretsDir + secret.Target
}
if secret.UID != "" || secret.GID != "" || secret.Mode != nil {
logrus.Warn("secrets `uid`, `gid` and `mode` are not supported, they will be ignored")
}
definedSecret := p.Secrets[secret.Source]
if definedSecret.External.External {
return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name)
@@ -928,10 +937,24 @@ func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount
return values, nil
}
func isAbsTarget(p string) bool {
return isUnixAbs(p) || isWindowsAbs(p)
}
func isUnixAbs(p string) bool {
return strings.HasPrefix(p, "/")
}
func isWindowsAbs(p string) bool {
if strings.HasPrefix(p, "\\\\") {
return true
}
if len(p) > 2 && p[1] == ':' {
return p[2] == '\\'
}
return false
}
func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
source := volume.Source
// on windows, filepath.IsAbs(source) is false for unix style abs path like /var/run/docker.sock.

View File

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

View File

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

View File

@@ -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\n stdout"))
_, err := c1Stdout.Write([]byte("hello stdout\n"))
assert.NoError(t, err, "Writing to fake stdout")
_, err = c1Stderr.Write([]byte("hello\n stderr"))
_, err = c1Stderr.Write([]byte("hello stderr\n"))
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"),
)
}

View File

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

View File

@@ -78,19 +78,48 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
}
}
var (
local int
mounts []string
)
for _, m := range container.Mounts {
name := m.Name
if name == "" {
name = m.Source
}
if m.Driver == "local" {
local++
}
mounts = append(mounts, name)
}
var networks []string
if container.NetworkSettings != nil {
for k := range container.NetworkSettings.Networks {
networks = append(networks, k)
}
}
summary[i] = api.ContainerSummary{
ID: container.ID,
Name: getCanonicalContainerName(container),
Image: container.Image,
Project: container.Labels[api.ProjectLabel],
Service: container.Labels[api.ServiceLabel],
Command: container.Command,
State: container.State,
Status: container.Status,
Created: container.Created,
Health: health,
ExitCode: exitCode,
Publishers: publishers,
ID: container.ID,
Name: getCanonicalContainerName(container),
Names: container.Names,
Image: container.Image,
Project: container.Labels[api.ProjectLabel],
Service: container.Labels[api.ServiceLabel],
Command: container.Command,
State: container.State,
Status: container.Status,
Created: container.Created,
Labels: container.Labels,
SizeRw: container.SizeRw,
SizeRootFs: container.SizeRootFs,
Mounts: mounts,
LocalVolumes: local,
Networks: networks,
Health: health,
ExitCode: exitCode,
Publishers: publishers,
}
return nil
})

View File

@@ -54,13 +54,34 @@ func TestPs(t *testing.T) {
containers, err := tested.Ps(ctx, strings.ToLower(testProject), compose.PsOptions{})
expected := []compose.ContainerSummary{
{ID: "123", Name: "123", Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: "running", Health: "healthy", Publishers: nil},
{ID: "456", Name: "456", Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
{ID: "123", Name: "123", Names: []string{"/123"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: "running", Health: "healthy", Publishers: nil,
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
compose.ServiceLabel: "service1",
},
},
{ID: "456", Name: "456", Names: []string{"/456"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: "running", Health: "",
Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90, PublishedPort: 80}}},
{ID: "789", Name: "789", Image: "foo", Project: strings.ToLower(testProject), Service: "service2",
State: "exited", Health: "", ExitCode: 130, Publishers: nil},
Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90, PublishedPort: 80}},
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
compose.ServiceLabel: "service1",
},
},
{ID: "789", Name: "789", Names: []string{"/789"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service2",
State: "exited", Health: "", ExitCode: 130, Publishers: nil,
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
compose.ServiceLabel: "service2",
},
},
}
assert.NilError(t, err)
assert.DeepEqual(t, containers, expected)

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,17 +56,48 @@ func (s *composeService) start(ctx context.Context, projectName string, options
}
}
eg, ctx := errgroup.WithContext(ctx)
// use an independent context tied to the errgroup for background attach operations
// the primary context is still used for other operations
// this means that once any attach operation fails, all other attaches are cancelled,
// but an attach failing won't interfere with the rest of the start
eg, attachCtx := errgroup.WithContext(ctx)
if listener != nil {
attached, err := s.attach(ctx, project, listener, options.AttachTo)
_, err := s.attach(attachCtx, project, listener, options.AttachTo)
if err != nil {
return err
}
eg.Go(func() error {
return s.watchContainers(context.Background(), project.Name, options.AttachTo, options.Services, listener, attached,
// it's possible to have a required service whose log output is not desired
// (i.e. it's not in the attach set), so watch everything and then filter
// calls to attach; this ensures that `watchContainers` blocks until all
// required containers have exited, even if their output is not being shown
attachTo := utils.NewSet[string](options.AttachTo...)
required := utils.NewSet[string](options.Services...)
toWatch := attachTo.Union(required).Elements()
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true, toWatch...)
if err != nil {
return err
}
// N.B. this uses the parent context (instead of attachCtx) so that the watch itself can
// continue even if one of the log streams fails
return s.watchContainers(ctx, project.Name, toWatch, required.Elements(), listener, containers,
func(container moby.Container, _ time.Time) error {
return s.attachContainer(ctx, container, listener)
svc := container.Labels[api.ServiceLabel]
if attachTo.Has(svc) {
return s.attachContainer(attachCtx, container, listener)
}
// HACK: simulate an "attach" event
listener(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: getContainerNameWithoutProject(container),
ID: container.ID,
Service: svc,
})
return nil
}, func(container moby.Container, _ time.Time) error {
listener(api.ContainerEvent{
Type: api.ContainerEventAttach,
@@ -108,6 +139,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
for _, s := range project.Services {
depends[s.Name] = types.ServiceDependency{
Condition: getDependencyCondition(s, project),
Required: true,
}
}
if options.WaitTimeout > 0 {
@@ -155,6 +187,13 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
required = services
}
unexpected := utils.NewSet[string](required...).Diff(utils.NewSet[string](services...))
if len(unexpected) != 0 {
return fmt.Errorf(`required service(s) "%s" not present in watched service(s) "%s"`,
strings.Join(unexpected.Elements(), ", "),
strings.Join(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 {
@@ -189,6 +228,12 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
err := s.Events(ctx, projectName, api.EventsOptions{
Services: services,
Consumer: func(event api.Event) error {
defer func() {
// after consuming each event, check to see if we're done
if len(expected) == 0 {
stop()
}
}()
inspected, err := s.apiClient().ContainerInspect(ctx, event.Container)
if err != nil {
if errdefs.IsNotFound(err) {
@@ -290,9 +335,6 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
}
}
}
if len(expected) == 0 {
stop()
}
return nil
},
})

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -17,13 +17,19 @@ package compose
import (
"context"
"fmt"
"io/fs"
"io"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
moby "github.com/docker/docker/api/types"
"github.com/docker/compose/v2/internal/sync"
"github.com/compose-spec/compose-go/types"
"github.com/jonboulle/clockwork"
"github.com/mitchellh/mapstructure"
@@ -32,7 +38,6 @@ import (
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v2/pkg/watch"
)
@@ -40,9 +45,11 @@ type DevelopmentConfig struct {
Watch []Trigger `json:"watch,omitempty"`
}
type WatchAction string
const (
WatchActionSync = "sync"
WatchActionRebuild = "rebuild"
WatchActionSync WatchAction = "sync"
WatchActionRebuild WatchAction = "rebuild"
)
type Trigger struct {
@@ -52,53 +59,44 @@ type Trigger struct {
Ignore []string `json:"ignore,omitempty"`
}
const quietPeriod = 2 * time.Second
const quietPeriod = 500 * time.Millisecond
// fileMapping contains the Compose service and modified host system path.
//
// For file sync, the container path is also included.
// For rebuild, there is no container path, so it is always empty.
type fileMapping struct {
// Service that the file event is for.
Service string
// HostPath that was created/modified/deleted outside the container.
//
// This is the path as seen from the user's perspective, e.g.
// - C:\Users\moby\Documents\hello-world\main.go
// - /Users/moby/Documents/hello-world/main.go
HostPath string
// ContainerPath for the target file inside the container (only populated
// for sync events, not rebuild).
//
// This is the path as used in Docker CLI commands, e.g.
// - /workdir/main.go
ContainerPath string
// fileEvent contains the Compose service and modified host system path.
type fileEvent struct {
sync.PathMapping
Action WatchAction
}
// getSyncImplementation returns the the tar-based syncer unless it has been explicitly
// disabled with `COMPOSE_EXPERIMENTAL_WATCH_TAR=0`. Note that the absence of the env
// var means enabled.
func (s *composeService) getSyncImplementation(project *types.Project) sync.Syncer {
var useTar bool
if useTarEnv, ok := os.LookupEnv("COMPOSE_EXPERIMENTAL_WATCH_TAR"); ok {
useTar, _ = strconv.ParseBool(useTarEnv)
} else {
useTar = true
}
if useTar {
return sync.NewTar(project.Name, tarDockerClient{s: s})
}
return sync.NewDockerCopy(project.Name, s, s.stdinfo())
}
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, _ api.WatchOptions) error { //nolint: gocyclo
needRebuild := make(chan fileMapping)
needSync := make(chan fileMapping)
_, err := s.prepareProjectForBuild(project, nil)
if err != nil {
return err
}
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
clock := clockwork.NewRealClock()
debounce(ctx, clock, quietPeriod, needRebuild, s.makeRebuildFn(ctx, project))
return nil
})
eg.Go(s.makeSyncFn(ctx, project, needSync))
ss, err := project.GetServices(services...)
if err != nil {
if err := project.ForServices(services); err != nil {
return err
}
syncer := s.getSyncImplementation(project)
eg, ctx := errgroup.WithContext(ctx)
watching := false
for _, service := range ss {
for i := range project.Services {
service := project.Services[i]
config, err := loadDevelopmentConfig(service, project)
if err != nil {
return err
@@ -122,7 +120,10 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
continue
}
name := service.Name
// set the service to always be built - watch triggers `Up()` when it receives a rebuild event
service.PullPolicy = types.PullPolicyBuild
project.Services[i] = service
dockerIgnores, err := watch.LoadDockerIgnore(service.Build.Context)
if err != nil {
return err
@@ -164,7 +165,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
eg.Go(func() error {
defer watcher.Close() //nolint:errcheck
return s.watch(ctx, name, watcher, config.Watch, needSync, needRebuild)
return s.watch(ctx, project, service.Name, watcher, syncer, config.Watch)
})
}
@@ -175,7 +176,17 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
return eg.Wait()
}
func (s *composeService) watch(ctx context.Context, name string, watcher watch.Notify, triggers []Trigger, needSync chan fileMapping, needRebuild chan fileMapping) error {
func (s *composeService) watch(
ctx context.Context,
project *types.Project,
name string,
watcher watch.Notify,
syncer sync.Syncer,
triggers []Trigger,
) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ignores := make([]watch.PathMatcher, len(triggers))
for i, trigger := range triggers {
ignore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
@@ -185,60 +196,82 @@ func (s *composeService) watch(ctx context.Context, name string, watcher watch.N
ignores[i] = ignore
}
WATCH:
events := make(chan fileEvent)
batchEvents := batchDebounceEvents(ctx, s.clock, quietPeriod, events)
go func() {
for {
select {
case <-ctx.Done():
return
case batch := <-batchEvents:
start := time.Now()
logrus.Debugf("batch start: service[%s] count[%d]", name, len(batch))
if err := s.handleWatchBatch(ctx, project, name, batch, syncer); err != nil {
logrus.Warnf("Error handling changed files for service %s: %v", name, err)
}
logrus.Debugf("batch complete: service[%s] duration[%s] count[%d]",
name, time.Since(start), len(batch))
}
}
}()
for {
select {
case <-ctx.Done():
return nil
case event := <-watcher.Events():
hostPath := event.Path()
for i, trigger := range triggers {
logrus.Debugf("change detected on %s - comparing with %s", hostPath, trigger.Path)
if watch.IsChild(trigger.Path, hostPath) {
match, err := ignores[i].Matches(hostPath)
if err != nil {
return err
}
if match {
logrus.Debugf("%s is matching ignore pattern", hostPath)
continue
}
fmt.Fprintf(s.stdinfo(), "change detected on %s\n", hostPath)
f := fileMapping{
HostPath: hostPath,
Service: name,
}
switch trigger.Action {
case WatchActionSync:
logrus.Debugf("modified file %s triggered sync", hostPath)
rel, err := filepath.Rel(trigger.Path, hostPath)
if err != nil {
return err
}
// always use Unix-style paths for inside the container
f.ContainerPath = path.Join(trigger.Target, rel)
needSync <- f
case WatchActionRebuild:
logrus.Debugf("modified file %s requires image to be rebuilt", hostPath)
needRebuild <- f
default:
return fmt.Errorf("watch action %q is not supported", trigger)
}
continue WATCH
}
}
case err := <-watcher.Errors():
return err
case event := <-watcher.Events():
hostPath := event.Path()
for i, trigger := range triggers {
logrus.Debugf("change for %s - comparing with %s", hostPath, trigger.Path)
if fileEvent := maybeFileEvent(trigger, hostPath, ignores[i]); fileEvent != nil {
events <- *fileEvent
}
}
}
}
}
// maybeFileEvent returns a file event object if hostPath is valid for the provided trigger and ignore
// rules.
//
// Any errors are logged as warnings and nil (no file event) is returned.
func maybeFileEvent(trigger Trigger, hostPath string, ignore watch.PathMatcher) *fileEvent {
if !watch.IsChild(trigger.Path, hostPath) {
return nil
}
isIgnored, err := ignore.Matches(hostPath)
if err != nil {
logrus.Warnf("error ignore matching %q: %v", hostPath, err)
return nil
}
if isIgnored {
logrus.Debugf("%s is matching ignore pattern", hostPath)
return nil
}
var containerPath string
if trigger.Target != "" {
rel, err := filepath.Rel(trigger.Path, hostPath)
if err != nil {
logrus.Warnf("error making %s relative to %s: %v", hostPath, trigger.Path, err)
return nil
}
// always use Unix-style paths for inside the container
containerPath = path.Join(trigger.Target, rel)
}
return &fileEvent{
Action: WatchAction(trigger.Action),
PathMapping: sync.PathMapping{
HostPath: hostPath,
ContainerPath: containerPath,
},
}
}
func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project) (*DevelopmentConfig, error) {
var config DevelopmentConfig
y, ok := service.Extensions["x-develop"]
@@ -249,16 +282,25 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
if err != nil {
return nil, err
}
baseDir, err := filepath.EvalSymlinks(project.WorkingDir)
if err != nil {
return nil, fmt.Errorf("resolving symlink for %q: %w", project.WorkingDir, err)
}
for i, trigger := range config.Watch {
if !filepath.IsAbs(trigger.Path) {
trigger.Path = filepath.Join(project.WorkingDir, trigger.Path)
trigger.Path = filepath.Join(baseDir, trigger.Path)
}
if p, err := filepath.EvalSymlinks(trigger.Path); err == nil {
// this might fail because the path doesn't exist, etc.
trigger.Path = p
}
trigger.Path = filepath.Clean(trigger.Path)
if trigger.Path == "" {
return nil, errors.New("watch rules MUST define a path")
}
if trigger.Action == WatchActionRebuild && service.Build == nil {
if trigger.Action == string(WatchActionRebuild) && service.Build == nil {
return nil, fmt.Errorf("service %s doesn't have a build section, can't apply 'rebuild' on watch", service.Name)
}
@@ -267,125 +309,54 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
return &config, nil
}
func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Project) func(services rebuildServices) {
for i, service := range project.Services {
service.PullPolicy = types.PullPolicyBuild
project.Services[i] = service
}
return func(services rebuildServices) {
serviceNames := make([]string, 0, len(services))
allPaths := make(utils.Set[string])
for serviceName, paths := range services {
serviceNames = append(serviceNames, serviceName)
for p := range paths {
allPaths.Add(p)
// batchDebounceEvents groups identical file events within a sliding time window and writes the results to the returned
// channel.
//
// The returned channel is closed when the debouncer is stopped via context cancellation or by closing the input channel.
func batchDebounceEvents(ctx context.Context, clock clockwork.Clock, delay time.Duration, input <-chan fileEvent) <-chan []fileEvent {
out := make(chan []fileEvent)
go func() {
defer close(out)
seen := make(map[fileEvent]time.Time)
flushEvents := func() {
if len(seen) == 0 {
return
}
events := make([]fileEvent, 0, len(seen))
for e := range seen {
events = append(events, e)
}
// sort batch by oldest -> newest
// (if an event is seen > 1 per batch, it gets the latest timestamp)
sort.SliceStable(events, func(i, j int) bool {
x := events[i]
y := events[j]
return seen[x].Before(seen[y])
})
out <- events
seen = make(map[fileEvent]time.Time)
}
fmt.Fprintf(
s.stdinfo(),
"Rebuilding %s after changes were detected:%s\n",
strings.Join(serviceNames, ", "),
strings.Join(append([]string{""}, allPaths.Elements()...), "\n - "),
)
err := s.Up(ctx, project, api.UpOptions{
Create: api.CreateOptions{
Services: serviceNames,
Inherit: true,
},
Start: api.StartOptions{
Services: serviceNames,
Project: project,
},
})
if err != nil {
fmt.Fprintf(s.stderr(), "Application failed to start after update\n")
}
}
}
func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project, needSync <-chan fileMapping) func() error {
return func() error {
t := clock.NewTicker(delay)
defer t.Stop()
for {
select {
case <-ctx.Done():
return nil
case opt := <-needSync:
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)
}
} else if errors.Is(statErr, fs.ErrNotExist) {
for i := 1; i <= scale; i++ {
_, err := s.Exec(ctx, project.Name, api.RunOptions{
Service: opt.Service,
Command: []string{"rm", "-rf", opt.ContainerPath},
Index: i,
})
if err != nil {
logrus.Warnf("failed to delete %q from %s: %v", opt.ContainerPath, opt.Service, err)
}
}
fmt.Fprintf(s.stdinfo(), "%s deleted from service\n", opt.ContainerPath)
return
case <-t.Chan():
flushEvents()
case e, ok := <-input:
if !ok {
// input channel was closed
flushEvents()
return
}
seen[e] = time.Now()
t.Reset(delay)
}
}
}
}
type rebuildServices map[string]utils.Set[string]
func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, input <-chan fileMapping, fn func(services rebuildServices)) {
services := make(rebuildServices)
t := clock.NewTimer(delay)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.Chan():
if len(services) > 0 {
go fn(services)
services = make(rebuildServices)
}
case e := <-input:
t.Reset(delay)
svc, ok := services[e.Service]
if !ok {
svc = make(utils.Set[string])
services[e.Service] = svc
}
svc.Add(e.HostPath)
}
}
}()
return out
}
func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
@@ -396,3 +367,149 @@ func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolu
}
return false
}
type tarDockerClient struct {
s *composeService
}
func (t tarDockerClient) ContainersForService(ctx context.Context, projectName string, serviceName string) ([]moby.Container, error) {
containers, err := t.s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
if err != nil {
return nil, err
}
return containers, nil
}
func (t tarDockerClient) Exec(ctx context.Context, containerID string, cmd []string, in io.Reader) error {
execCfg := moby.ExecConfig{
Cmd: cmd,
AttachStdout: false,
AttachStderr: true,
AttachStdin: in != nil,
Tty: false,
}
execCreateResp, err := t.s.apiClient().ContainerExecCreate(ctx, containerID, execCfg)
if err != nil {
return err
}
startCheck := moby.ExecStartCheck{Tty: false, Detach: false}
conn, err := t.s.apiClient().ContainerExecAttach(ctx, execCreateResp.ID, startCheck)
if err != nil {
return err
}
defer conn.Close()
var eg errgroup.Group
if in != nil {
eg.Go(func() error {
defer func() {
_ = conn.CloseWrite()
}()
_, err := io.Copy(conn.Conn, in)
return err
})
}
eg.Go(func() error {
_, err := io.Copy(t.s.stdinfo(), conn.Reader)
return err
})
err = t.s.apiClient().ContainerExecStart(ctx, execCreateResp.ID, startCheck)
if err != nil {
return err
}
// although the errgroup is not tied directly to the context, the operations
// in it are reading/writing to the connection, which is tied to the context,
// so they won't block indefinitely
if err := eg.Wait(); err != nil {
return err
}
execResult, err := t.s.apiClient().ContainerExecInspect(ctx, execCreateResp.ID)
if err != nil {
return err
}
if execResult.Running {
return errors.New("process still running")
}
if execResult.ExitCode != 0 {
return fmt.Errorf("exit code %d", execResult.ExitCode)
}
return nil
}
func (s *composeService) handleWatchBatch(
ctx context.Context,
project *types.Project,
serviceName string,
batch []fileEvent,
syncer sync.Syncer,
) error {
pathMappings := make([]sync.PathMapping, len(batch))
for i := range batch {
if batch[i].Action == WatchActionRebuild {
fmt.Fprintf(
s.stdinfo(),
"Rebuilding %s after changes were detected:%s\n",
serviceName,
strings.Join(append([]string{""}, batch[i].HostPath), "\n - "),
)
err := s.Up(ctx, project, api.UpOptions{
Create: api.CreateOptions{
Services: []string{serviceName},
Inherit: true,
},
Start: api.StartOptions{
Services: []string{serviceName},
Project: project,
},
})
if err != nil {
fmt.Fprintf(s.stderr(), "Application failed to start after update\n")
}
return nil
}
pathMappings[i] = batch[i].PathMapping
}
writeWatchSyncMessage(s.stdinfo(), serviceName, pathMappings)
service, err := project.GetService(serviceName)
if err != nil {
return err
}
if err := syncer.Sync(ctx, service, pathMappings); err != nil {
return err
}
return nil
}
// writeWatchSyncMessage prints out a message about the sync for the changed paths.
func writeWatchSyncMessage(w io.Writer, serviceName string, pathMappings []sync.PathMapping) {
const maxPathsToShow = 10
if len(pathMappings) <= maxPathsToShow || logrus.IsLevelEnabled(logrus.DebugLevel) {
hostPathsToSync := make([]string, len(pathMappings))
for i := range pathMappings {
hostPathsToSync[i] = pathMappings[i].HostPath
}
fmt.Fprintf(
w,
"Syncing %s after changes were detected:%s\n",
serviceName,
strings.Join(append([]string{""}, hostPathsToSync...), "\n - "),
)
} else {
hostPathsToSync := make([]string, len(pathMappings))
for i := range pathMappings {
hostPathsToSync[i] = pathMappings[i].HostPath
}
fmt.Fprintf(
w,
"Syncing %s after %d changes were detected\n",
serviceName,
len(pathMappings),
)
}
}

View File

@@ -16,45 +16,60 @@ package compose
import (
"context"
"os"
"testing"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/watch"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/mocks"
moby "github.com/docker/docker/api/types"
"github.com/golang/mock/gomock"
"github.com/jonboulle/clockwork"
"golang.org/x/sync/errgroup"
"github.com/stretchr/testify/require"
"github.com/docker/compose/v2/internal/sync"
"github.com/docker/compose/v2/pkg/watch"
"gotest.tools/v3/assert"
)
func Test_debounce(t *testing.T) {
ch := make(chan fileMapping)
var (
ran int
got []string
)
func TestDebounceBatching(t *testing.T) {
ch := make(chan fileEvent)
clock := clockwork.NewFakeClock()
ctx, stop := context.WithCancel(context.Background())
t.Cleanup(stop)
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
debounce(ctx, clock, quietPeriod, ch, func(services rebuildServices) {
for svc := range services {
got = append(got, svc)
}
ran++
stop()
})
return nil
})
eventBatchCh := batchDebounceEvents(ctx, clock, quietPeriod, ch)
for i := 0; i < 100; i++ {
ch <- fileMapping{Service: "test"}
var action WatchAction = "a"
if i%2 == 0 {
action = "b"
}
ch <- fileEvent{Action: action}
}
assert.Equal(t, ran, 0)
// we sent 100 events + the debouncer
clock.BlockUntil(101)
clock.Advance(quietPeriod)
err := eg.Wait()
assert.NilError(t, err)
assert.Equal(t, ran, 1)
assert.DeepEqual(t, got, []string{"test"})
select {
case batch := <-eventBatchCh:
require.ElementsMatch(t, batch, []fileEvent{
{Action: "a"},
{Action: "b"},
})
case <-time.After(50 * time.Millisecond):
t.Fatal("timed out waiting for events")
}
clock.BlockUntil(1)
clock.Advance(quietPeriod)
// there should only be a single batch
select {
case batch := <-eventBatchCh:
t.Fatalf("unexpected events: %v", batch)
case <-time.After(50 * time.Millisecond):
// channel is empty
}
}
type testWatcher struct {
@@ -78,73 +93,106 @@ func (t testWatcher) Errors() chan error {
return t.errors
}
func Test_sync(t *testing.T) {
needSync := make(chan fileMapping)
needRebuild := make(chan fileMapping)
ctx, cancelFunc := context.WithCancel(context.TODO())
defer cancelFunc()
func TestWatch_Sync(t *testing.T) {
mockCtrl := gomock.NewController(t)
cli := mocks.NewMockCli(mockCtrl)
cli.EXPECT().Err().Return(os.Stderr).AnyTimes()
apiClient := mocks.NewMockAPIClient(mockCtrl)
apiClient.EXPECT().ContainerList(gomock.Any(), gomock.Any()).Return([]moby.Container{
testContainer("test", "123", false),
}, nil).AnyTimes()
cli.EXPECT().Client().Return(apiClient).AnyTimes()
run := func() watch.Notify {
watcher := testWatcher{
events: make(chan watch.FileEvent, 1),
errors: make(chan error),
}
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
go func() {
cli, err := command.NewDockerCli()
assert.NilError(t, err)
service := composeService{
dockerCli: cli,
}
err = service.watch(ctx, "test", watcher, []Trigger{
{
Path: "/src",
Action: "sync",
Target: "/work",
Ignore: []string{"ignore"},
},
{
Path: "/",
Action: "rebuild",
},
}, needSync, needRebuild)
assert.NilError(t, err)
}()
return watcher
proj := types.Project{
Services: []types.ServiceConfig{
{
Name: "test",
},
},
}
t.Run("synchronize file", func(t *testing.T) {
watcher := run()
watcher.Events() <- watch.NewFileEvent("/src/changed")
select {
case actual := <-needSync:
assert.DeepEqual(t, fileMapping{Service: "test", HostPath: "/src/changed", ContainerPath: "/work/changed"}, actual)
case <-time.After(100 * time.Millisecond):
t.Error("timeout")
}
})
watcher := testWatcher{
events: make(chan watch.FileEvent),
errors: make(chan error),
}
t.Run("ignore", func(t *testing.T) {
watcher := run()
watcher.Events() <- watch.NewFileEvent("/src/ignore")
select {
case <-needSync:
t.Error("file event should have been ignored")
case <-time.After(100 * time.Millisecond):
// expected
syncer := newFakeSyncer()
clock := clockwork.NewFakeClock()
go func() {
service := composeService{
dockerCli: cli,
clock: clock,
}
})
err := service.watch(ctx, &proj, "test", watcher, syncer, []Trigger{
{
Path: "/sync",
Action: "sync",
Target: "/work",
Ignore: []string{"ignore"},
},
{
Path: "/rebuild",
Action: "rebuild",
},
})
assert.NilError(t, err)
}()
t.Run("rebuild", func(t *testing.T) {
watcher := run()
watcher.Events() <- watch.NewFileEvent("/dependencies.yaml")
select {
case event := <-needRebuild:
assert.Equal(t, "test", event.Service)
case <-time.After(100 * time.Millisecond):
t.Error("timeout")
}
})
watcher.Events() <- watch.NewFileEvent("/sync/changed")
watcher.Events() <- watch.NewFileEvent("/sync/changed/sub")
clock.BlockUntil(3)
clock.Advance(quietPeriod)
select {
case actual := <-syncer.synced:
require.ElementsMatch(t, []sync.PathMapping{
{HostPath: "/sync/changed", ContainerPath: "/work/changed"},
{HostPath: "/sync/changed/sub", ContainerPath: "/work/changed/sub"},
}, actual)
case <-time.After(100 * time.Millisecond):
t.Error("timeout")
}
watcher.Events() <- watch.NewFileEvent("/sync/ignore")
watcher.Events() <- watch.NewFileEvent("/sync/ignore/sub")
watcher.Events() <- watch.NewFileEvent("/sync/changed")
clock.BlockUntil(4)
clock.Advance(quietPeriod)
select {
case actual := <-syncer.synced:
require.ElementsMatch(t, []sync.PathMapping{
{HostPath: "/sync/changed", ContainerPath: "/work/changed"},
}, actual)
case <-time.After(100 * time.Millisecond):
t.Error("timed out waiting for events")
}
watcher.Events() <- watch.NewFileEvent("/rebuild")
watcher.Events() <- watch.NewFileEvent("/sync/changed")
clock.BlockUntil(4)
clock.Advance(quietPeriod)
select {
case batch := <-syncer.synced:
t.Fatalf("received unexpected events: %v", batch)
case <-time.After(100 * time.Millisecond):
// expected
}
// TODO: there's not a great way to assert that the rebuild attempt happened
}
type fakeSyncer struct {
synced chan []sync.PathMapping
}
func newFakeSyncer() *fakeSyncer {
return &fakeSyncer{
synced: make(chan []sync.PathMapping),
}
}
func (f *fakeSyncer) Sync(_ context.Context, _ types.ServiceConfig, paths []sync.PathMapping) error {
f.synced <- paths
return nil
}

View File

@@ -29,18 +29,16 @@ import (
func RequireServiceState(t testing.TB, cli *CLI, service string, state string) {
t.Helper()
psRes := cli.RunDockerComposeCmd(t, "ps", "--format=json", service)
var psOut []map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(psRes.Stdout()), &psOut),
var svc map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(psRes.Stdout()), &svc),
"Invalid `compose ps` JSON output")
for _, svc := range psOut {
require.Equal(t, service, svc["Service"],
"Found ps output for unexpected service")
require.Equalf(t,
strings.ToLower(state),
strings.ToLower(svc["State"].(string)),
"Service %q (%s) not in expected state",
service, svc["Name"],
)
}
require.Equal(t, service, svc["Service"],
"Found ps output for unexpected service")
require.Equalf(t,
strings.ToLower(state),
strings.ToLower(svc["State"].(string)),
"Service %q (%s) not in expected state",
service, svc["Name"],
)
}

View File

@@ -19,7 +19,9 @@ package e2e
import (
"fmt"
"net/http"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"time"
@@ -36,8 +38,8 @@ func TestLocalComposeBuild(t *testing.T) {
t.Run(env+" build named and unnamed images", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "custom-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "build")
@@ -48,8 +50,8 @@ func TestLocalComposeBuild(t *testing.T) {
t.Run(env+" build with build-arg", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "custom-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "build", "--build-arg", "FOO=BAR")
@@ -59,8 +61,8 @@ func TestLocalComposeBuild(t *testing.T) {
t.Run(env+" build with build-arg set by env", func(t *testing.T) {
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "custom-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
icmd.RunCmd(c.NewDockerComposeCmd(t,
"--project-directory",
@@ -70,7 +72,7 @@ func TestLocalComposeBuild(t *testing.T) {
"FOO"),
func(cmd *icmd.Cmd) {
cmd.Env = append(cmd.Env, "FOO=BAR")
})
}).Assert(t, icmd.Success)
res := c.RunDockerCmd(t, "image", "inspect", "build-test-nginx")
res.Assert(t, icmd.Expected{Out: `"FOO": "BAR"`})
@@ -90,8 +92,9 @@ func TestLocalComposeBuild(t *testing.T) {
})
t.Run(env+" build as part of up", func(t *testing.T) {
c.RunDockerOrExitError(t, "rmi", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "custom-nginx")
// ensure local test run does not reuse previously build image
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
t.Cleanup(func() {
@@ -111,7 +114,7 @@ func TestLocalComposeBuild(t *testing.T) {
t.Run(env+" no rebuild when up again", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d")
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"), res.Stdout())
assert.Assert(t, !strings.Contains(res.Stdout(), "COPY static"))
})
t.Run(env+" rebuild when up --build", func(t *testing.T) {
@@ -121,10 +124,15 @@ func TestLocalComposeBuild(t *testing.T) {
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
})
t.Run(env+" build --push ignored for unnamed images", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--workdir", "fixtures/build-test", "build", "--push", "nginx")
assert.Assert(t, !strings.Contains(res.Stdout(), "failed to push"), res.Stdout())
})
t.Run(env+" cleanup build project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
c.RunDockerCmd(t, "rmi", "build-test-nginx")
c.RunDockerCmd(t, "rmi", "custom-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
c.RunDockerOrExitError(t, "rmi", "-f", "custom-nginx")
})
}
@@ -361,10 +369,21 @@ func TestBuildPrivileged(t *testing.T) {
})
t.Run("use build privileged mode to run insecure build command", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/privileged", "build")
assert.NilError(t, res.Error, res.Stderr())
res.Assert(t, icmd.Expected{Out: "CapEff:\t0000003fffffffff"})
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/privileged", "build")
capEffRe := regexp.MustCompile("CapEff:\t([0-9a-f]+)")
matches := capEffRe.FindStringSubmatch(res.Stdout())
assert.Equal(t, 2, len(matches), "Did not match CapEff in output, matches: %v", matches)
capEff, err := strconv.ParseUint(matches[1], 16, 64)
assert.NilError(t, err, "Parsing CapEff: %s", matches[1])
// NOTE: can't use constant from x/sys/unix or tests won't compile on macOS/Windows
// #define CAP_SYS_ADMIN 21
// https://github.com/torvalds/linux/blob/v6.1/include/uapi/linux/capability.h#L278
const capSysAdmin = 0x15
if capEff&capSysAdmin != capSysAdmin {
t.Fatalf("CapEff %s is missing CAP_SYS_ADMIN", matches[1])
}
})
}

View File

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

View File

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

View File

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

View File

@@ -51,8 +51,18 @@ func TestUpExitCodeFrom(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-exit-code-from"
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--exit-code-from=failure", "failure")
res.Assert(t, icmd.Expected{ExitCode: 42})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}
func TestUpExitCodeFromContainerKilled(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-exit-code-from-kill"
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/start-fail/start-depends_on-long-lived.yaml", "--project-name", projectName, "up", "--exit-code-from=test")
res.Assert(t, icmd.Expected{ExitCode: 137})
res.Assert(t, icmd.Expected{ExitCode: 143})
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}
@@ -61,10 +71,14 @@ func TestPortRange(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-port-range"
reset := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans", "--timeout=0")
}
reset()
t.Cleanup(reset)
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/port-range/compose.yaml", "--project-name", projectName, "up", "-d")
res.Assert(t, icmd.Success)
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
}
func TestStdoutStderr(t *testing.T) {

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More