Compare commits

...

135 Commits

Author SHA1 Message Date
Roberto Villarreal
04b8ac5fe4 Unquote volume names in creation events
Volumes are the only resources that are quoted, and only on creation.

Signed-off-by: Roberto Villarreal <rrjjvv@yahoo.com>
2025-08-29 11:50:42 +02:00
dependabot[bot]
d09948da41 build(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.10.0 to 1.11.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.10.0...v1.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-29 08:28:51 +02:00
Sebastiaan van Stijn
f1efbb8322 use enum-consts for State and Health
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-29 08:18:16 +02:00
Sebastiaan van Stijn
1d52012b82 go.mod: bump buildkit v0.24.0-rc1, buildx v0.28.0-rc1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-29 08:16:23 +02:00
Sebastiaan van Stijn
1d69f4a68c pkg/compose: composeService.Up: rewrite without go-multierror
- Use a errgroup.Group and add a appendErr utility to not fail-fast,
  but collect errors.
- replace doneCh for a global context to cancel goroutines
- Commented out attachCtx code, as it didn't appear to be functional
  (as it wouldn't be cancelled).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-27 15:25:17 +02:00
Kian Eliasi
6078b4d99d Fix: use image created time when last tag time is not present
Signed-off-by: Kian Eliasi <kian.elbo@gmail.com>
2025-08-27 09:03:58 +02:00
Kian Eliasi
73e593e69a Fix: incorrect time when last tag time is not set
Signed-off-by: Kian Eliasi <kian.elbo@gmail.com>
2025-08-27 09:03:58 +02:00
Sebastiaan van Stijn
51499f645b pkg/compose: pull: use native multi-errors
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-25 11:22:25 +02:00
Sebastiaan van Stijn
5165b0f814 internal/tracing: replace go-multierror.Group with sync.WaitGroup
The go-multierror Group is just a shallow wrapper around sync.WaitGroup;
https://github.com/hashicorp/go-multierror/blob/v1.1.1/group.go#L5-L38

This patch replaces the go-multierror.Group for a sync.WaitGroup (we
probably don't need to limit concurrency for this one) and stdlib multi-
errors.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-25 11:22:25 +02:00
Sebastiaan van Stijn
93dd1a4558 internal/sync: replace go-multierror.Group with golang.org/x/sync/errgroup
The go-multierror Group is just a shallow wrapper around sync.WaitGroup;
https://github.com/hashicorp/go-multierror/blob/v1.1.1/group.go#L5-L38

It does not limit concurrency, but handles synchronisation to collect
all errors (if any) in a go-multierror.

This patch replaces the go-multierror.Group for a sync.ErrGroup (which
is slightly easier to use, and does allow for limiting concurrency if
wanted), and a basic slice with mutex to collect the errors and to produce
a stdlib multi-error through errors.Join

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-25 11:22:25 +02:00
Sebastiaan van Stijn
ba3f5664c0 cmd/formatter: remove unused SetMultiErrorFormat
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-25 11:22:25 +02:00
Guillaume Lours
c420bc44c4 check the assume yes publish flag command before the presence of bind mounts
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-08-25 10:29:52 +02:00
Max Proske
60681a824c Add e2e test to verify docker compose down works even when env file is missing
Signed-off-by: Max Proske <max@mproske.com>
2025-08-25 10:03:55 +02:00
Max Proske
19ad737ee7 Fix runtime ops with missing env file
Signed-off-by: Max Proske <max@mproske.com>
2025-08-25 10:03:55 +02:00
may
d3a260e533 add completions for the --progress flag
Signed-off-by: may <m4rch3n1ng@gmail.com>
2025-08-25 10:01:49 +02:00
dependabot[bot]
e75329dce2 build(deps): bump go.uber.org/mock from 0.5.2 to 0.6.0
Bumps [go.uber.org/mock](https://github.com/uber/mock) from 0.5.2 to 0.6.0.
- [Release notes](https://github.com/uber/mock/releases)
- [Changelog](https://github.com/uber-go/mock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uber/mock/compare/v0.5.2...v0.6.0)

---
updated-dependencies:
- dependency-name: go.uber.org/mock
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 09:50:16 +02:00
Sebastiaan van Stijn
1dc0be2c30 go.mod: github.com/docker/buildx v0.27.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-25 09:36:33 +02:00
cuiweixie
3bac9ffd08 Refactor to use maps.Copy
Signed-off-by: cuiweixie <cuiweixie@gmail.com>
2025-08-25 08:57:52 +02:00
Guillaume Lours
f266715dd0 add --provenance and --sbom flag to generated bake command line,
also add attestation per-service configuration to generated bake target

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-08-13 09:36:22 +02:00
Guillaume Lours
c2cb0aef6b only monitor attached services on up command
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-08-08 17:27:08 +02:00
Austin Vazquez
fbc62d111e bump golang to 1.23.12
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
2025-08-08 16:22:30 +02:00
dependabot[bot]
0d40064ce8 build(deps): bump golang.org/x/sys from 0.34.0 to 0.35.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.34.0 to 0.35.0.
- [Commits](https://github.com/golang/sys/compare/v0.34.0...v0.35.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-08 14:22:18 +02:00
dependabot[bot]
91a6eafa1d build(deps): bump github.com/docker/go-connections from 0.5.0 to 0.6.0
Bumps [github.com/docker/go-connections](https://github.com/docker/go-connections) from 0.5.0 to 0.6.0.
- [Commits](https://github.com/docker/go-connections/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/docker/go-connections
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-08 14:08:26 +02:00
Matthew Runyon
f36ee00f71 Add go as a prerequesite in build instructions
Signed-off-by: Matthew Runyon <matthewrunyon@deephaven.io>
2025-08-06 15:27:09 +02:00
dependabot[bot]
29ede3ba7d build(deps): bump github.com/containerd/containerd/v2
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.3 to 2.1.4.
- [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/v2.1.3...v2.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-06 13:55:31 +02:00
Guillaume Lours
bf6d7bf47e define pull and no_cache from either service or flags values when building with bake
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-08-05 13:52:43 +02:00
Sebastiaan van Stijn
fc66da06db pkg/compose: simplify getting auth-config key
Rewrite to remove the `github.com/docker/docker/registry` dependency,
which will not be included in the upcoming "api" and "client" modules,
and will not be a public package in the module used for the daemon itself.

1. don't call "/info" API endpoint to get default registry

The `IndexServerAddress` in the `/info` endpoint was added as part of the
initial Windows implementation of the engine. For legal reasons, Microsoft
Windows (and thus Docker images based on Windows) were not allowed to be
distributed through non-Microsoft infrastructure. As a temporary solution,
a dedicated "registry-win-tp3.docker.io" registry was created to serve
Windows images.

Using separate registries was not an ideal solution, and a more permanent
solution was created by introducing "foreign image layers" in the distribution
spec, after which the "registry-win-tp3.docker.io" ceased to exist, and
removed from the engine through docker/docker PR 21100.

However, the `ElectAuthServer` was left in place, quoting from that PR;

> make the client check which default registry the daemon uses is still
> more correct than leaving it up to the client, even if it won't technically
> matter after this PR. There may be some backward compatibility scenarios
> where `ElectAuthServer` [sic] is still helpful.

That comment was 10 Years ago, and the CLI stopped using this information,
as the default registry is not configurable, so in practice was a static
value. (see b4ca1c7368).

2. replace `ParseRepositoryInfo` and `GetAuthConfigKey` with local impl

The `ParseRepositoryInfo` function was originally implemented for use by
the daemon itself. It returns a `RepositoryInfo` struct that holds information
about the repository and the registry the repository can be found in.

As it was written for use by the daemon, it also was designed to be used
in combination with the daemon's configuration (such as mirrors, and
insecure registries). If no daemon configuration is present, which would
be the case when used in a CLI, it uses fallback logic as used in the daemon
to detect if the registry is running on a localhost / loopback address,
because such addresses are allowed to be "insecure" by default; this includes
resolving the IP-address of the host (if it's not an IP-address).

Unfortunately, these functions (and related types) were reused in the
CLI and many other places, which resulted in those types to be deeply
ingrained in interfaces and (external) code.

For compose; it was only used to get the "auth-config key" to use for
looking up auth information from the credentials store, which still
needs special handling for the "default" (docker hub) domain, which
unlike other image references doesn't use the hostname included in
the image reference for the actual registry (and key for storing
auth).

For those that want to follow along;

First, note that `GetAuthConfig` only requires a `registry.IndexInfo`, so not
the whole `RepositoryInfo` struct;
https://github.com/moby/moby/blob/v28.3.3/registry/types.go#L8-L24

From the `registry.IndexInfo` it only uses the `IsOfficial` and `Name` fields;
https://github.com/moby/moby/blob/v28.3.3/registry/config.go#L390-L395

But to get the `IndexInfo`, `ParseRepositoryInfo` is needed, which first
takes the image reference's "domain name" (e.g. `docker.io`);
https://github.com/moby/moby/blob/v28.3.3/registry/config.go#L421

This gets "normalized" for some cases where the `info.IndexServerAddress`
was incorrectly assumed to be the canonical domain for Docker Hub registry,
and which _does_ happen to also be accessible as a "v2" registry.
https://github.com/moby/moby/blob/v28.3.3/registry/config.go#L334-L341

After normalizing, it checks if it's a docker hub address ("docker.io"
after normalizing); Docker Hub is always required to use a secure
connection, so no detection happens, and the `Official` field is set
to indicate it's Docker Hub (this code path was already simplified
as historically it would try to find daemon configuration (or otherwise
use a default) for Mirror configuration;
https://github.com/moby/moby/blob/v28.3.3/registry/config.go#L420-L443

For non-Docker Hub registries, it also sets the name, and attempts
to detect if the registry is allowed to be "insecure";
https://github.com/moby/moby/blob/v28.3.3/registry/config.go#L435-L442

Which (as mentioned) involves parsing the address and, if needed, resolving
the hostname
https://github.com/moby/moby/blob/v28.3.3/registry/config.go#L445-L481

As `Insecure` is not used for looking up the auth-config key, all of the
above can be reduced to;

- Is the hostname obtained from the image reference "docker.io" (after normalizing)?
- If so, use the special `https://index.docker.io/v1/` as auth-config key (another horrible remnant)
- Otherwise use the hostname obtained from the image reference as-is

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-05 10:08:23 +02:00
Sebastiaan van Stijn
909211dd61 use cli-plugins/metadata package
The metadata types and consts where moved to a separate package,
so update the code to use the new location instead of the aliases
provided.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-08-05 10:01:53 +02:00
dependabot[bot]
0dc9852c67 build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.3.2+incompatible to 28.3.3+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.2...v28.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 11:39:57 +02:00
dependabot[bot]
a478702236 build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.3.2+incompatible to 28.3.3+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.3.2...v28.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 11:27:29 +02:00
Nicolas De Loof
2c12ad19db use log API for containers we didn't attached to
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-29 14:23:24 +02:00
Guillaume Lours
038ea8441a apply BUILDKIT_PROGRESS value when building with bake
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-29 11:45:33 +02:00
Guillaume Lours
9e98e6101e add missing _MODEL suffix to model variable pass to dependent services of a model
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-29 09:40:46 +02:00
keitosuwahara
52f04229c0 fixed lint error
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-28 10:07:03 +02:00
keitosuwahara
28895d0322 fix lint error
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-28 10:07:03 +02:00
keitosuwahara
a926f7d717 Elimneted magic string
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-28 10:07:03 +02:00
Nicolas De Loof
fe046915eb buildkit require os.Stdout to access the raw terminal
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-28 09:25:35 +02:00
keitosuwahara
adbd61e5d6 fixed lint error
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-27 19:36:40 +02:00
keitosuwahara
e37ac04329 deleted useless comment
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-27 19:36:40 +02:00
keitosuwahara
cab2c2a44e Refactoring of redundant condition checks
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-27 19:36:40 +02:00
keitosuwahara
1946de598d improved lint error
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-27 15:12:38 +02:00
keitosuwahara
8e29a138aa improved test
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-27 15:12:38 +02:00
keitosuwahara
3c8da0afee Add test of json.go
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-27 15:12:38 +02:00
keitosuwahara
1b12c867c5 add Streams Comment
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-27 14:57:43 +02:00
Guillaume Lours
1a4fc55fd7 bump compose-go to v2.8.1
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-24 18:10:41 +02:00
Guillaume Lours
efc939dcee add info about models usage to OpenTelemetry spans
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-24 16:01:28 +02:00
keitosuwahara
d6e9f79ba6 Integration of SetAttributes calls
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-24 09:05:30 +02:00
keitosuwahara
b4c44a431f Eliminate magic number in init functions
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-24 09:04:52 +02:00
keitosuwahara
fb5a8644c3 Efficiency of ansiColorCode function
Signed-off-by: keitosuwahara <keitosuwahara0816@gmail.com>
2025-07-24 09:04:02 +02:00
Guillaume Lours
95660c5e5a bump buildx to v0.26.1
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-23 16:01:28 +02:00
Guillaume Lours
f6ddd6ae88 use output registry when push true and load to docker store if not
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-23 15:49:19 +02:00
dependabot[bot]
4ae7066955 build(deps): bump google.golang.org/grpc from 1.73.0 to 1.74.2
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.73.0 to 1.74.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.73.0...v1.74.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 14:51:08 +02:00
Nicolas De Loof
fd954f266c show build progress during watch rebuild
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-23 13:57:46 +02:00
Nicolas De Loof
d62e21025c forward git command error to user
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-23 12:10:25 +02:00
Guillaume Lours
6a2d16bd10 bump compose-go to version v2.8.0
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-23 11:53:47 +02:00
Guillaume Lours
4d47da6dc2 do not pass user id on Windows system as engine is not able to handel it
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-22 12:07:58 +02:00
Nicolas De Loof
8f91793fb5 introduce build.provenance and sbom support
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-21 17:07:41 +02:00
Sebastiaan van Stijn
1d2223fb23 pkg/compose: use local copy of pkg/system.IsAbs
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-21 16:02:52 +02:00
Sebastiaan van Stijn
d4f6000712 remove import aliases for containerd/errdefs
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-21 15:51:39 +02:00
Sebastiaan van Stijn
c50d16cd78 pkg/compose: remove uses of moby/errdefs
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-21 15:51:39 +02:00
Nicolas De Loof
3875e13fad simpler stop UI
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-21 10:31:50 +02:00
Nicolas De Loof
c89f30170d force plain displaymode if stdout isn't a terminal
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-21 10:19:58 +02:00
Nicolas De Loof
41a9b91887 warn user COMPOSE_BAKE=false is deprecated
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-21 10:18:08 +02:00
Nicolas De Loof
5fc2b2a71c
fix yaml indentation
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-18 11:59:56 +02:00
Nicolas De Loof
b1cd40c316 swarm
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-18 11:58:25 +02:00
dependabot[bot]
362ab0733f build(deps): bump github.com/spf13/pflag from 1.0.6 to 1.0.7
Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.6...v1.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-17 15:35:38 +02:00
Nicolas De Loof
f35d2cfb3b monitor must watch events even when context is cancelled
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-17 15:08:35 +02:00
Nicolas De Loof
17ba6c7188 abstract model-cli commands execution with a model (pseudo) API
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-16 17:34:59 +02:00
Nicolas De Loof
1c37f1abb6 use logs API with Since to collect the very first logs after restart
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-16 17:24:11 +02:00
Nicolas De Loof
485b6200ee (refactoring) introduce monitor to manage containers events and application termination
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-16 17:24:11 +02:00
Nicolas De Loof
8c17a35609 don't run navigation menu if stdin isn't a terminal
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-16 16:51:26 +02:00
Guillaume Lours
6b9667401a fix the helm bridge e2e tests after the latest update of the templates
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-16 16:04:52 +02:00
Arthur Bols
9a1e589ce8 Fix report image name in bake result
Signed-off-by: Arthur Bols <arthur@bols.dev>
2025-07-16 08:59:12 +02:00
Guillaume Lours
5e147e852e add default compose labels to images built from bake
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-16 07:35:07 +02:00
Nicolas De Loof
29308cb97e keep containers attached on stop to capture termination logs
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-15 11:26:44 +02:00
Guillaume Lours
0b0242d0ac add dry-run support to bake build
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-15 10:52:03 +02:00
Cedric Staniewski
5a704004d3 Add a space character to separate the timestamp from the log message
Signed-off-by: Cedric Staniewski <cedric@gmx.ca>
2025-07-15 10:50:43 +02:00
MohammadHasan Akbari
cb95910018 chore: print model attribute instead of model name used in compose file
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
2025-07-11 11:08:08 +02:00
MohammadHasan Akbari
f42226e352 feat: add --models flag to config command
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
2025-07-11 11:08:08 +02:00
Nicolas De Loof
0cc3c7a550 bump dependencies
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-11 11:04:26 +02:00
atagtm
f7ee9c8a0c feat(os): add FreeBSD support
Signed-off-by: atagtm <donisos1146@gmail.com>
2025-07-11 10:44:45 +02:00
MohammadHasan Akbari
35efa97b7d feat: add since & until flags to events command
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
Co-authored-by: Amin Ehterami <A.Ehterami@proton.me>
2025-07-09 10:08:33 +02:00
dependabot[bot]
9e17a091be build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.3.0+incompatible to 28.3.1+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.0...v28.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-08 11:17:18 +02:00
Guillaume Lours
4bbc6c609f add USER_AGENT variable to cmd when shellouting
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-08 11:12:40 +02:00
Nicolas De Loof
69f1430a49 resolve Dockerfile symlink but file name
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-08 10:06:34 +02:00
Nicolas De Loof
7cf7c6414f build resolves enabled service after project has been loaded
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-07 16:43:42 +02:00
MohammadHasan Akbari
0e0ed91a39 fix: lint errors
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
2025-07-07 10:37:20 +02:00
MohammadHasan Akbari
66524e7728 feat: add --networks flag to config command
Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
2025-07-07 10:37:20 +02:00
Guillaume Lours
c626befee1 fix the way we're checking if the provider metadata are empty or not
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-04 12:41:37 +02:00
Nicolas De Loof
60ee6adcd2 a single place for shell-out command setup
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-04 11:17:11 +02:00
dependabot[bot]
8faf1eb808 build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.3.0+incompatible to 28.3.1+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.3.0...v28.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-03 11:35:58 +02:00
dependabot[bot]
3b0601b671 build(deps): bump github.com/moby/buildkit from 0.23.1 to 0.23.2
Bumps [github.com/moby/buildkit](https://github.com/moby/buildkit) from 0.23.1 to 0.23.2.
- [Release notes](https://github.com/moby/buildkit/releases)
- [Commits](https://github.com/moby/buildkit/compare/v0.23.1...v0.23.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 11:49:11 +02:00
Guillaume Lours
f42374bb18 add a Done event to model progress display
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-02 11:25:07 +02:00
mountdisk
b046a5ef72 chore: fix some minor issues in the comments
Signed-off-by: mountdisk <mountdisk@icloud.com>
2025-07-02 07:31:00 +02:00
Guillaume Lours
1570c6c076 bump go-viper/mapstructure to version v2.3.0
Fix https://github.com/advisories/GHSA-fv92-fjc5-jj9h

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-01 14:56:57 +02:00
Guillaume Lours
674e13fb6d bump golang to v1.23.10
Fix Vulnerability Report: GO-2025-3751

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-07-01 14:52:43 +02:00
Nicolas De Loof
6fa173124a (reactoring) avoid a global variable by introducing logConsumer decorator
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-07-01 12:29:05 +02:00
Sebastiaan van Stijn
2c69fc3d4d pkg/compose: remove redundant uses of strslice.StrSlice
The strslice.StrSlice type is a string-slice with a custom JSON Unmarshal
function to provide backward-compatibility with older API requests (see
[moby@17d6f00] and [moby@ea4a067]).

Given that the type is assigned implicitly through the fields on HostConfig,
we can just use a regular []string instead.

[moby@17d6f00]: 17d6f00ec2
[moby@ea4a067]: ea4a06740b

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-01 10:49:29 +02:00
Nicolas De Loof
317ebcd3b0 implement model_variable
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-30 21:50:39 +02:00
Guillaume Lours
5cf1f0e2a4 bump compose-go to version v2.7.1
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-30 21:22:56 +02:00
Leonardo Peregrino
6198ed5bd2 fix linting errors
Signed-off-by: Leonardo Peregrino <55335068+leoperegrino@users.noreply.github.com>
2025-06-30 19:38:27 +02:00
Leonardo Peregrino
00ccddbde8 add volumes command test cases
Signed-off-by: Leonardo Peregrino <55335068+leoperegrino@users.noreply.github.com>
2025-06-30 19:38:27 +02:00
Leonardo Peregrino
63b441401e add volumes docs/reference/
Signed-off-by: Leonardo Peregrino <55335068+leoperegrino@users.noreply.github.com>
2025-06-30 19:38:27 +02:00
Leonardo Peregrino
3a7982fe45 add service filter to volumes command
Signed-off-by: Leonardo Peregrino <55335068+leoperegrino@users.noreply.github.com>
2025-06-30 19:38:27 +02:00
Leonardo Peregrino
5430caa172 add volumes command
Signed-off-by: Leonardo Peregrino <55335068+leoperegrino@users.noreply.github.com>
2025-06-30 19:38:27 +02:00
Nicolas De Loof
ee1b1e0a93 run docker model configure
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-30 19:22:35 +02:00
Nicolas De Loof
26e46d7cc8 e2e tests in CI
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-30 19:22:35 +02:00
Nicolas De Loof
a9e76943f6 introduce support for models
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-30 19:22:35 +02:00
Nicolas De Loof
b6a0df8d3c e2e compose run --env
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-30 19:09:22 +02:00
Guillaume Lours
5a063b7510 fix provider concurrent environment map accesses
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-30 19:07:10 +02:00
dependabot[bot]
ae49bba9be build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.2.2+incompatible to 28.3.0+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.2.2...v28.3.0)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.3.0+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 18:49:27 +02:00
Nicolas De Loof
51acc58453 mount /var/run/docker.sock for --use-api-socket
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-30 18:45:39 +02:00
Guillaume Lours
7c999d7f93 improve publish bind mount warning message
Co-authored-by: Nicolas De loof <nicolas.deloof@gmail.com>
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-30 18:45:30 +02:00
Guillaume Lours
ad750d6143 remove publish limitation on bind mount
list all bind mounts and ask user validation before publishing

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-30 18:45:30 +02:00
dependabot[bot]
fe382df507 build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.2.2+incompatible to 28.3.0+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.2.2...v28.3.0)

---
updated-dependencies:
- dependency-name: github.com/docker/cli
  dependency-version: 28.3.0+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 18:37:19 +02:00
Nicolas De Loof
6501d59efc pass project.environment to bake
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-30 15:39:55 +02:00
Zhizhen He
33a782572f fix: typos
Signed-off-by: Zhizhen He <hezhizhen.yi@gmail.com>
2025-06-30 14:26:17 +02:00
Guillaume Lours
65803ea12e remove error message from exec outpout by default
Add the error as a log for verbose mode

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-30 12:50:03 +02:00
Guillaume Lours
f613379373 make sure the post_start hooks fails
before we were assuming the container will be close before the post_start will be executed

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-30 12:37:58 +02:00
Guillaume Lours
3553aa26a6 add a default statut messsage to exec error to avoid empty line display
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-27 20:44:49 +02:00
Nathan Scott
257ea7b75d Swap to Reader in bake to avoid hangs on output
Signed-off-by: Nathan Scott <natedscott@gmail.com>
2025-06-27 16:19:28 +02:00
Guillaume Lours
d219aa66f4 don't fail down cmd if services with pre_stop hook already stopped/removed
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-27 14:41:52 +02:00
Guillaume Lours
c9ebfad78e exclude provider services from the list of dependencies that Compose should wait for
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-26 21:26:33 +02:00
Guillaume Lours
8e57362a0f use errdefs.IsNotImplemented to check if the logging is not implemented
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-26 16:42:57 +02:00
Guillaume Lours
29630f184e check progress default value instead of empty string to use BUILDKIT_PROGRESS env variable value
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-26 16:42:57 +02:00
Nicolas De Loof
6514c680a5 only expose API socket to service asking for it
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-24 17:17:41 +02:00
Nicolas De Loof
3394bf031b propagate target docker host set by --host to Bake
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-24 15:51:45 +02:00
dependabot[bot]
832a08f579 build(deps): bump github.com/moby/buildkit from 0.23.0 to 0.23.1
Bumps [github.com/moby/buildkit](https://github.com/moby/buildkit) from 0.23.0 to 0.23.1.
- [Release notes](https://github.com/moby/buildkit/releases)
- [Commits](https://github.com/moby/buildkit/compare/v0.23.0...v0.23.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-24 15:39:44 +02:00
Nicolas De Loof
aadce87b16 inject secrets/config just before container is started
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-24 12:26:46 +02:00
Nicolas De Loof
b3207c455d setting buildOptions.Services triggers image to be always rebuilt
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-24 11:26:52 +02:00
Guillaume Lours
769b7391ba don't create from run command during dependencies creation process
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-24 11:26:52 +02:00
Guillaume Lours
149b882ebf don't create from run command during dependencies creation process
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-24 11:26:52 +02:00
Sebastiaan van Stijn
c97e40e2b8 pkg/compose: remove uses of ExecOptions.Detach
This field was added in [moby@5130fe5d38837302e], which
added it for use as intermediate struct when parsing CLI flags (through
`runconfig.ParseExec`) in [moby@c786a8ee5e9db8f5f].

Commit [moby@9d9dff3d0d9e92adf] rewrote the CLI to use
Cobra, and as part of this introduced a separate `execOptions` type in
`api/client/container`, however the ExecOptions.Detach field was still
used as intermediate field to store the flag's value.

Given that the client doesn't use this field, let's remove its use to
prevent giving the impression that it's used anywhere.

[moby@5130fe5d38837302e]: 5130fe5d38
[moby@c786a8ee5e9db8f5f]: c786a8ee5e
[moby@9d9dff3d0d9e92adf]: 9d9dff3d0d

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-23 21:05:38 +02:00
Nicolas De Loof
22e23bd4dc networkMode
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-22 20:50:35 +02:00
Nicolas De Loof
2dde5faeb8 add support for cache_to with bake
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-22 07:44:38 +02:00
132 changed files with 2893 additions and 1139 deletions

View File

@ -12,6 +12,12 @@ body:
Include both the current behavior (what you are seeing) as well as what you expected to happen.
validations:
required: true
- type: markdown
attributes:
value: |
[Docker Swarm](https://www.mirantis.com/software/swarm/) uses a distinct compose file parser and
as such doesn't support some of the recent features of Docker Compose. Please contact Mirantis
if you need assistance with compose file support in Docker Swarm.
- type: textarea
attributes:
label: Steps To Reproduce

View File

@ -183,6 +183,11 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Model
run: |
sudo apt-get install docker-model-plugin
docker model version
- name: Set up Go
uses: actions/setup-go@v5
with:
@ -190,6 +195,9 @@ jobs:
check-latest: true
cache: true
- name: Build example provider
run: make example-provider
- name: Build
uses: docker/bake-action@v6
with:

View File

@ -30,6 +30,8 @@ linters:
deny:
- pkg: io/ioutil
desc: io/ioutil package has been deprecated
- pkg: github.com/docker/docker/errdefs
desc: use github.com/containerd/errdefs instead.
- pkg: golang.org/x/exp/maps
desc: use stdlib maps package
- pkg: golang.org/x/exp/slices

View File

@ -4,12 +4,15 @@
* Windows:
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/windows-install/)
* make
* go (see [go.mod](go.mod) for minimum version)
* macOS:
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/)
* make
* go (see [go.mod](go.mod) for minimum version)
* Linux:
* [Docker 20.10 or later](https://docs.docker.com/engine/install/)
* make
* go (see [go.mod](go.mod) for minimum version)
### Building the CLI

View File

@ -15,7 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
ARG GO_VERSION=1.23.8
ARG GO_VERSION=1.23.12
ARG XX_VERSION=1.6.1
ARG GOLANGCI_LINT_VERSION=v2.0.2
ARG ADDLICENSE_VERSION=v1.0.0

View File

@ -74,7 +74,7 @@ install: binary
install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose
.PHONY: e2e-compose
e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
e2e-compose: example-provider ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 ./pkg/e2e
.PHONY: e2e-compose-standalone
@ -87,6 +87,10 @@ build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and r
.PHONY: build-and-e2e-compose-standalone
build-and-e2e-compose-standalone: build e2e-compose-standalone ## Compile the compose cli-plugin and run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
.PHONY: example-provider
example-provider: ## build example provider for e2e tests
go build -o bin/build/example-provider docs/examples/provider.go
.PHONY: mocks
mocks:
mockgen --version >/dev/null 2>&1 || go install go.uber.org/mock/mockgen@v0.4.0

View File

@ -23,6 +23,12 @@ your application are configured.
Once you have a Compose file, you can create and start your application with a
single command: `docker compose up`.
> **Note**: About Docker Swarm
> Docker Swarm used to rely on the legacy compose file format but did not adopted the compose specification
> so is missing some of the recent enhancements in the compose syntax. After
> [acquisition by Mirantis](https://www.mirantis.com/software/swarm/) swarm isn't maintained by Docker Inc, and
> as such some Docker Compose features aren't accessible to swarm users.
# Where to get Docker Compose
### Windows and macOS

View File

@ -55,8 +55,10 @@ func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
ctx,
"cli/"+strings.Join(commandName(cmd), "-"),
)
cmdSpan.SetAttributes(attribute.StringSlice("cli.flags", getFlags(cmd.Flags())))
cmdSpan.SetAttributes(attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()))
cmdSpan.SetAttributes(
attribute.StringSlice("cli.flags", getFlags(cmd.Flags())),
attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()),
)
cmd.SetContext(ctx)
wrapRunE(cmd, cmdSpan, tracingShutdown)

View File

@ -45,7 +45,8 @@ type buildOptions struct {
deps bool
print bool
check bool
provenance bool
sbom string
provenance string
}
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
@ -84,6 +85,7 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
Check: opts.check,
SSHs: SSHKeys,
Builder: builderName,
SBOM: opts.sbom,
Provenance: opts.provenance,
}, nil
}
@ -119,12 +121,14 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
}
flags := cmd.Flags()
flags.BoolVar(&opts.push, "push", false, "Push service images")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the build output")
flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image")
flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services")
flags.StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)")
flags.StringVar(&opts.builder, "builder", "", "Set builder to use")
flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively)")
flags.StringVar(&opts.provenance, "provenance", "", `Add a provenance attestation`)
flags.StringVar(&opts.sbom, "sbom", "", `Add a SBOM attestation`)
flags.Bool("parallel", true, "Build images in parallel. DEPRECATED")
flags.MarkHidden("parallel") //nolint:errcheck
@ -145,6 +149,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
}
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
opts.All = true // do not drop resources as build may involve some dependencies by additional_contexts
project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
if err != nil {
return err
@ -155,7 +160,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o
}
apiBuildOptions, err := opts.toAPIBuildOptions(services)
apiBuildOptions.Provenance = true
apiBuildOptions.Attestations = true
if err != nil {
return err
}

View File

@ -36,7 +36,7 @@ import (
composegoutils "github.com/compose-spec/compose-go/v2/utils"
"github.com/docker/buildx/util/logutil"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/pkg/kvfile"
"github.com/docker/compose/v2/cmd/formatter"
@ -245,7 +245,7 @@ func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cl
name := o.ProjectName
var project *types.Project
if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile)
p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
if err != nil {
envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" {
@ -416,7 +416,7 @@ const PluginName = "compose"
// RunningAsStandalone detects when running as a standalone program
func RunningAsStandalone() bool {
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName && os.Args[1] != PluginName
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName && os.Args[1] != PluginName
}
// RootCommand returns the compose command with its child commands
@ -636,6 +636,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
publishCommand(&opts, dockerCli, backend),
alphaCommand(&opts, dockerCli, backend),
bridgeCommand(&opts, dockerCli),
volumesCommand(&opts, dockerCli, backend),
)
c.Flags().SetInterspersed(false)
@ -660,6 +661,10 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
"profile",
completeProfileNames(dockerCli, &opts),
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"progress",
cobra.FixedCompletions(printerModes, cobra.ShellCompDirectiveNoFileComp),
)
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
c.Flags().IntVar(&parallel, "parallel", -1, `Control max parallelism, -1 for unlimited`)

View File

@ -50,6 +50,8 @@ type configOptions struct {
noResolveEnv bool
services bool
volumes bool
networks bool
models bool
profiles bool
images bool
hash string
@ -111,6 +113,12 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
if opts.volumes {
return runVolumes(ctx, dockerCli, opts)
}
if opts.networks {
return runNetworks(ctx, dockerCli, opts)
}
if opts.models {
return runModels(ctx, dockerCli, opts)
}
if opts.hash != "" {
return runHash(ctx, dockerCli, opts)
}
@ -147,6 +155,8 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.")
flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.")
flags.BoolVar(&opts.networks, "networks", false, "Print the network names, one per line.")
flags.BoolVar(&opts.models, "models", false, "Print the model names, one per line.")
flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.")
flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.")
flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.")
@ -367,6 +377,30 @@ func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions)
return nil
}
func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for n := range project.Networks {
_, _ = fmt.Fprintln(dockerCli.Out(), n)
}
return nil
}
func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
for _, model := range project.Models {
if model.Model != "" {
_, _ = fmt.Fprintln(dockerCli.Out(), model.Model)
}
}
return nil
}
func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
var services []string
if opts.hash != "*" {

View File

@ -29,7 +29,9 @@ import (
type eventsOpts struct {
*composeOptions
json bool
json bool
since string
until string
}
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
@ -48,6 +50,8 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
}
cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects")
cmd.Flags().StringVar(&opts.since, "since", "", "Show all events created since timestamp")
cmd.Flags().StringVar(&opts.until, "until", "", "Stream events until this timestamp")
return cmd
}
@ -59,6 +63,8 @@ func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service,
return backend.Events(ctx, name, api.EventsOptions{
Services: services,
Since: opts.since,
Until: opts.until,
Consumer: func(event api.Event) error {
if opts.json {
marshal, err := json.Marshal(map[string]interface{}{

View File

@ -18,12 +18,16 @@ package compose
import (
"context"
"errors"
"fmt"
"os"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -59,7 +63,15 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runExec(ctx, dockerCli, backend, opts)
err := runExec(ctx, dockerCli, backend, opts)
if err != nil {
logrus.Debugf("%v", err)
var cliError cli.StatusError
if ok := errors.As(err, &cliError); ok {
os.Exit(err.(cli.StatusError).StatusCode) //nolint: errorlint
}
}
return err
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@ -109,8 +121,8 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, op
exitCode, err := backend.Exec(ctx, projectName, execOpts)
if exitCode != 0 {
errMsg := ""
if err != nil {
errMsg := fmt.Sprintf("exit status %d", exitCode)
if err != nil && err.Error() != "" {
errMsg = err.Error()
}
return cli.StatusError{StatusCode: exitCode, Status: errMsg}

View File

@ -101,6 +101,10 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
// Convert map to slice
var imageList []img
for ctr, i := range images {
lastTagTime := i.LastTagTime
if lastTagTime.IsZero() {
lastTagTime = i.Created
}
imageList = append(imageList, img{
ContainerName: ctr,
ID: i.ID,
@ -108,7 +112,7 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
Tag: i.Tag,
Platform: platforms.Format(i.Platform),
Size: i.Size,
LastTagTime: i.LastTagTime,
LastTagTime: lastTagTime,
})
}
json, err := formatter.ToJSON(imageList, "", "")

View File

@ -282,7 +282,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
var buildForRun *api.BuildOptions
if !createOpts.noBuild {
bo, err := buildOpts.toAPIBuildOptions(project.ServiceNames())
bo, err := buildOpts.toAPIBuildOptions(nil)
if err != nil {
return err
}

View File

@ -165,6 +165,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers")
flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information")
flags.BoolVar(&build.quiet, "quiet-build", false, "Suppress the build output")
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")
@ -223,6 +224,7 @@ func validateFlags(up *upOptions, create *createOptions) error {
return nil
}
//nolint:gocyclo
func runUp(
ctx context.Context,
dockerCli command.Cli,
@ -330,7 +332,7 @@ func runUp(
WaitTimeout: timeout,
Watch: upOptions.watch,
Services: services,
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain",
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain" && dockerCli.In().IsTerminal(),
},
})
}

95
cmd/compose/volumes.go Normal file
View File

@ -0,0 +1,95 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"slices"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
type volumesOptions struct {
*ProjectOptions
Quiet bool
Format string
}
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
options := volumesOptions{
ProjectOptions: p,
}
cmd := &cobra.Command{
Use: "volumes [OPTIONS] [SERVICE...]",
Short: "List volumes",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runVol(ctx, dockerCli, backend, args, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display volume names")
cmd.Flags().StringVar(&options.Format, "format", "table", flags.FormatHelp)
return cmd
}
func runVol(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, options volumesOptions) error {
project, _, err := options.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
names := project.ServiceNames()
if len(services) == 0 {
services = names
}
for _, service := range services {
if !slices.Contains(names, service) {
return fmt.Errorf("no such service: %s", service)
}
}
volumes, err := backend.Volumes(ctx, project, api.VolumesOptions{
Services: services,
})
if err != nil {
return err
}
if options.Quiet {
for _, v := range volumes {
_, _ = fmt.Fprintln(dockerCli.Out(), v.Name)
}
return nil
}
volumeCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewVolumeFormat(options.Format, options.Quiet),
}
return formatter.VolumeWrite(volumeCtx, volumes)
}

View File

@ -28,49 +28,42 @@ func ansi(code string) string {
return fmt.Sprintf("\033%s", code)
}
func SaveCursor() {
func saveCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("7"))
}
func RestoreCursor() {
func restoreCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("8"))
}
func HideCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("[?25l"))
}
func ShowCursor() {
func showCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("[?25h"))
}
func MoveCursor(y, x int) {
func moveCursor(y, x int) {
if disableAnsi {
return
}
fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
}
func MoveCursorX(pos int) {
func carriageReturn() {
if disableAnsi {
return
}
fmt.Print(ansi(fmt.Sprintf("[%dG", pos)))
fmt.Print(ansi(fmt.Sprintf("[%dG", 0)))
}
func ClearLine() {
func clearLine() {
if disableAnsi {
return
}
@ -78,7 +71,7 @@ func ClearLine() {
fmt.Print(ansi("[2K"))
}
func MoveCursorUp(lines int) {
func moveCursorUp(lines int) {
if disableAnsi {
return
}
@ -86,7 +79,7 @@ func MoveCursorUp(lines int) {
fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
}
func MoveCursorDown(lines int) {
func moveCursorDown(lines int) {
if disableAnsi {
return
}
@ -94,7 +87,7 @@ func MoveCursorDown(lines int) {
fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
}
func NewLine() {
func newLine() {
// Like \n
fmt.Print("\012")
}

View File

@ -19,6 +19,7 @@ package formatter
import (
"fmt"
"strconv"
"strings"
"sync"
"github.com/docker/cli/cli/command"
@ -58,6 +59,9 @@ const (
Auto = "auto"
)
// ansiColorOffset is the offset for basic foreground colors in ANSI escape codes.
const ansiColorOffset = 30
// SetANSIMode configure formatter for colored output on ANSI-compliant console
func SetANSIMode(streams command.Streams, ansi string) {
if !useAnsi(streams, ansi) {
@ -91,11 +95,15 @@ func ansiColor(code, s string, formatOpts ...string) string {
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
func ansiColorCode(code string, formatOpts ...string) string {
res := "\033["
var sb strings.Builder
sb.WriteString("\033[")
for _, c := range formatOpts {
res = fmt.Sprintf("%s%s;", res, c)
sb.WriteString(c)
sb.WriteString(";")
}
return fmt.Sprintf("%s%sm", res, code)
sb.WriteString(code)
sb.WriteString("m")
return sb.String()
}
func makeColorFunc(code string) colorFunc {
@ -122,8 +130,8 @@ func rainbowColor() colorFunc {
func init() {
colors := map[string]colorFunc{}
for i, name := range names {
colors[name] = makeColorFunc(strconv.Itoa(30 + i))
colors["intense_"+name] = makeColorFunc(strconv.Itoa(30+i) + ";1")
colors[name] = makeColorFunc(strconv.Itoa(ansiColorOffset + i))
colors["intense_"+name] = makeColorFunc(strconv.Itoa(ansiColorOffset+i) + ";1")
}
rainbow = []colorFunc{
colors["cyan"],

View File

@ -56,10 +56,6 @@ func NewLogConsumer(ctx context.Context, stdout, stderr io.Writer, color, prefix
}
}
func (l *logConsumer) Register(name string) {
l.register(name)
}
func (l *logConsumer) register(name string) *presenter {
var p *presenter
root, _, found := strings.Cut(name, " ")
@ -73,9 +69,12 @@ func (l *logConsumer) register(name string) *presenter {
} else {
cf := monochrome
if l.color {
if name == api.WatchLogger {
switch name {
case "":
cf = monochrome
case api.WatchLogger:
cf = makeColorFunc("92")
} else {
default:
cf = nextColor()
}
}
@ -118,23 +117,15 @@ func (l *logConsumer) write(w io.Writer, container, message string) {
if l.ctx.Err() != nil {
return
}
if KeyboardManager != nil {
KeyboardManager.ClearKeyboardInfo()
}
p := l.getPresenter(container)
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
for _, line := range strings.Split(message, "\n") {
if l.timestamp {
_, _ = fmt.Fprintf(w, "%s%s%s\n", p.prefix, timestamp, line)
_, _ = fmt.Fprintf(w, "%s%s %s\n", p.prefix, timestamp, line)
} else {
_, _ = fmt.Fprintf(w, "%s%s\n", p.prefix, line)
}
}
if KeyboardManager != nil {
KeyboardManager.PrintKeyboardInfo()
}
}
func (l *logConsumer) Status(container, msg string) {
@ -168,3 +159,27 @@ func (p *presenter) setPrefix(width int) {
}
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name))
}
type logDecorator struct {
decorated api.LogConsumer
Before func()
After func()
}
func (l logDecorator) Log(containerName, message string) {
l.Before()
l.decorated.Log(containerName, message)
l.After()
}
func (l logDecorator) Err(containerName, message string) {
l.Before()
l.decorated.Err(containerName, message)
l.After()
}
func (l logDecorator) Status(container, msg string) {
l.Before()
l.decorated.Status(container, msg)
l.After()
}

View File

@ -22,7 +22,6 @@ import (
"fmt"
"math"
"os"
"reflect"
"syscall"
"time"
@ -49,8 +48,8 @@ func (ke *KeyboardError) printError(height int, info string) {
if ke.shouldDisplay() {
errMessage := ke.err.Error()
MoveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
ClearLine()
moveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
clearLine()
fmt.Print(errMessage)
}
@ -70,12 +69,12 @@ func (ke *KeyboardError) error() string {
}
type KeyboardWatch struct {
Watching bool
Watcher Toggle
IsConfigured bool
Watching bool
Watcher Feature
}
type Toggle interface {
// Feature is an compose feature that can be started/stopped by a menu command
type Feature interface {
Start(context.Context) error
Stop() error
}
@ -90,31 +89,26 @@ const (
type LogKeyboard struct {
kError KeyboardError
Watch KeyboardWatch
Watch *KeyboardWatch
IsDockerDesktopActive bool
logLevel KEYBOARD_LOG_LEVEL
signalChannel chan<- os.Signal
}
// FIXME(ndeloof) we should avoid use of such a global reference. see use in logConsumer
var KeyboardManager *LogKeyboard
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal, w bool, watcher Toggle) *LogKeyboard {
KeyboardManager = &LogKeyboard{
Watch: KeyboardWatch{
Watching: w,
Watcher: watcher,
IsConfigured: !reflect.ValueOf(watcher).IsNil(),
},
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal) *LogKeyboard {
return &LogKeyboard{
IsDockerDesktopActive: isDockerDesktopActive,
logLevel: INFO,
signalChannel: sc,
}
return KeyboardManager
}
func (lk *LogKeyboard) ClearKeyboardInfo() {
lk.clearNavigationMenu()
func (lk *LogKeyboard) Decorate(l api.LogConsumer) api.LogConsumer {
return logDecorator{
decorated: l,
Before: lk.clearNavigationMenu,
After: lk.PrintKeyboardInfo,
}
}
func (lk *LogKeyboard) PrintKeyboardInfo() {
@ -139,7 +133,7 @@ func (lk *LogKeyboard) createBuffer(lines int) {
if lines > 0 {
allocateSpace(lines)
MoveCursorUp(lines)
moveCursorUp(lines)
}
}
@ -152,17 +146,17 @@ func (lk *LogKeyboard) printNavigationMenu() {
height := goterm.Height()
menu := lk.navigationMenu()
MoveCursorX(0)
SaveCursor()
carriageReturn()
saveCursor()
lk.kError.printError(height, menu)
MoveCursor(height-extraLines(menu), 0)
ClearLine()
moveCursor(height-extraLines(menu), 0)
clearLine()
fmt.Print(menu)
MoveCursorX(0)
RestoreCursor()
carriageReturn()
restoreCursor()
}
}
@ -185,7 +179,7 @@ func (lk *LogKeyboard) navigationMenu() string {
watchInfo = navColor(" ")
}
isEnabled := " Enable"
if lk.Watch.Watching {
if lk.Watch != nil && lk.Watch.Watching {
isEnabled = " Disable"
}
watchInfo = watchInfo + shortcutKeyColor("w") + navColor(isEnabled+" Watch")
@ -194,15 +188,15 @@ func (lk *LogKeyboard) navigationMenu() string {
func (lk *LogKeyboard) clearNavigationMenu() {
height := goterm.Height()
MoveCursorX(0)
SaveCursor()
carriageReturn()
saveCursor()
// ClearLine()
// clearLine()
for i := 0; i < height; i++ {
MoveCursorDown(1)
ClearLine()
moveCursorDown(1)
clearLine()
}
RestoreCursor()
restoreCursor()
}
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
@ -268,7 +262,7 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
}
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
if !lk.Watch.IsConfigured {
if lk.Watch == nil {
return
}
if lk.Watch.Watching {
@ -299,7 +293,7 @@ func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEv
case 'v':
lk.openDockerDesktop(ctx, project)
case 'w':
if !lk.Watch.IsConfigured {
if lk.Watch == nil {
// we try to open watch docs if DD is installed
if lk.IsDockerDesktopActive {
lk.openDDWatchDocs(ctx, project)
@ -322,22 +316,29 @@ func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEv
case keyboard.KeyCtrlC:
_ = keyboard.Close()
lk.clearNavigationMenu()
ShowCursor()
showCursor()
lk.logLevel = NONE
// will notify main thread to kill and will handle gracefully
lk.signalChannel <- syscall.SIGINT
case keyboard.KeyEnter:
NewLine()
newLine()
lk.printNavigationMenu()
}
}
func (lk *LogKeyboard) EnableWatch(enabled bool, watcher Feature) {
lk.Watch = &KeyboardWatch{
Watching: enabled,
Watcher: watcher,
}
}
func allocateSpace(lines int) {
for i := 0; i < lines; i++ {
ClearLine()
NewLine()
MoveCursorX(0)
clearLine()
newLine()
carriageReturn()
}
}

View File

@ -20,7 +20,7 @@ import (
"os"
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/cmdtrace"
@ -68,7 +68,7 @@ func pluginMain() {
})
return cmd
},
manager.Metadata{
metadata.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: internal.Version,

View File

@ -39,20 +39,30 @@ func main() {
}
}
type options struct {
db string
size int
}
func composeCommand() *cobra.Command {
c := &cobra.Command{
Use: "compose EVENT",
TraverseChildren: true,
}
c.PersistentFlags().String("project-name", "", "compose project name") // unused
var options options
upCmd := &cobra.Command{
Use: "up",
Run: up,
Use: "up",
Run: func(_ *cobra.Command, args []string) {
up(options, args)
},
Args: cobra.ExactArgs(1),
}
upCmd.Flags().String("type", "", "Database type (mysql, postgres, etc.)")
upCmd.Flags().StringVar(&options.db, "type", "", "Database type (mysql, postgres, etc.)")
_ = upCmd.MarkFlagRequired("type")
upCmd.Flags().Int("size", 10, "Database size in GB")
upCmd.Flags().IntVar(&options.size, "size", 10, "Database size in GB")
upCmd.Flags().String("name", "", "Name of the database to be created")
_ = upCmd.MarkFlagRequired("name")
@ -71,13 +81,13 @@ func composeCommand() *cobra.Command {
const lineSeparator = "\n"
func up(_ *cobra.Command, args []string) {
func up(options options, args []string) {
servicename := args[0]
fmt.Printf(`{ "type": "debug", "message": "Starting %s" }%s`, servicename, lineSeparator)
for i := 0; i < 100; i += 10 {
for i := 0; i < options.size; i += 10 {
time.Sleep(1 * time.Second)
fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i, lineSeparator)
fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i*100/options.size, lineSeparator)
}
fmt.Printf(`{ "type": "setenv", "message": "URL=https://magic.cloud/%s" }%s`, servicename, lineSeparator)
}

View File

@ -43,6 +43,7 @@ Define and run multi-container applications with Docker
| [`unpause`](compose_unpause.md) | Unpause services |
| [`up`](compose_up.md) | Create and start containers |
| [`version`](compose_version.md) | Show the Docker Compose version information |
| [`volumes`](compose_volumes.md) | List volumes |
| [`wait`](compose_wait.md) | Block until containers of all (or specified) services stop. |
| [`watch`](compose_watch.md) | Watch build context for service and rebuild/refresh containers when files are updated |

View File

@ -22,9 +22,11 @@ run `docker compose build` to rebuild it.
| `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. |
| `--no-cache` | `bool` | | Do not use cache when building the image |
| `--print` | `bool` | | Print equivalent bake file |
| `--provenance` | `string` | | Add a provenance attestation |
| `--pull` | `bool` | | Always attempt to pull a newer version of the image |
| `--push` | `bool` | | Push service images |
| `-q`, `--quiet` | `bool` | | Don't print anything to STDOUT |
| `-q`, `--quiet` | `bool` | | Suppress the build output |
| `--sbom` | `string` | | Add a SBOM attestation |
| `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) |
| `--with-dependencies` | `bool` | | Also build dependencies (transitively) |

View File

@ -15,6 +15,8 @@ the canonical format.
| `--hash` | `string` | | Print the service config hash, one per line. |
| `--images` | `bool` | | Print the image names, one per line. |
| `--lock-image-digests` | `bool` | | Produces an override file with image digests |
| `--models` | `bool` | | Print the model names, one per line. |
| `--networks` | `bool` | | Print the network names, one per line. |
| `--no-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output |
| `--no-env-resolution` | `bool` | | Don't resolve service env files |
| `--no-interpolate` | `bool` | | Don't interpolate environment variables |

View File

@ -23,10 +23,12 @@ The events that can be received using this can be seen [here](/reference/cli/doc
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--json` | `bool` | | Output events as a stream of json objects |
| Name | Type | Default | Description |
|:------------|:---------|:--------|:------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--json` | `bool` | | Output events as a stream of json objects |
| `--since` | `string` | | Show all events created since timestamp |
| `--until` | `string` | | Stream events until this timestamp |
<!---MARKER_GEN_END-->

View File

@ -44,6 +44,7 @@ If the process is interrupted using `SIGINT` (ctrl + C) or `SIGTERM`, the contai
| `--no-recreate` | `bool` | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
| `--no-start` | `bool` | | Don't start the services after creating them |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
| `--quiet-build` | `bool` | | Suppress the build output |
| `--quiet-pull` | `bool` | | Pull without printing progress information |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `-V`, `--renew-anon-volumes` | `bool` | | Recreate anonymous volumes instead of retrieving data from the previous containers |

View File

@ -0,0 +1,16 @@
# docker compose volumes
<!---MARKER_GEN_START-->
List volumes
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--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` | `bool` | | Only display volume names |
<!---MARKER_GEN_END-->

View File

@ -37,6 +37,7 @@ cname:
- docker compose unpause
- docker compose up
- docker compose version
- docker compose volumes
- docker compose wait
- docker compose watch
clink:
@ -72,6 +73,7 @@ clink:
- docker_compose_unpause.yaml
- docker_compose_up.yaml
- docker_compose_version.yaml
- docker_compose_volumes.yaml
- docker_compose_wait.yaml
- docker_compose_watch.yaml
options:

View File

@ -125,6 +125,15 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: provenance
value_type: string
description: Add a provenance attestation
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: pull
value_type: bool
default_value: "false"
@ -149,7 +158,16 @@ options:
shorthand: q
value_type: bool
default_value: "false"
description: Don't print anything to STDOUT
description: Suppress the build output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: sbom
value_type: string
description: Add a SBOM attestation
deprecated: false
hidden: false
experimental: false

View File

@ -56,6 +56,26 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: models
value_type: bool
default_value: "false"
description: Print the model names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: networks
value_type: bool
default_value: "false"
description: Print the network names, one per line.
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-consistency
value_type: bool
default_value: "false"

View File

@ -34,6 +34,24 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: since
value_type: string
description: Show all events created since timestamp
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: until
value_type: string
description: Stream events until this timestamp
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool

View File

@ -211,6 +211,16 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-build
value_type: bool
default_value: "false"
description: Suppress the build output
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-pull
value_type: bool
default_value: "false"

View File

@ -0,0 +1,52 @@
command: docker compose volumes
short: List volumes
long: List volumes
usage: docker compose volumes [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: format
value_type: string
default_value: table
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
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only display volume names
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

71
go.mod
View File

@ -1,6 +1,6 @@
module github.com/docker/compose/v2
go 1.23.8
go 1.23.12
require (
github.com/AlecAivazis/survey/v2 v2.3.7
@ -8,28 +8,27 @@ require (
github.com/Microsoft/go-winio v0.6.2
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/buger/goterm v1.0.4
github.com/compose-spec/compose-go/v2 v2.6.5
github.com/containerd/containerd/v2 v2.1.2
github.com/compose-spec/compose-go/v2 v2.8.1
github.com/containerd/containerd/v2 v2.1.4
github.com/containerd/errdefs v1.0.0
github.com/containerd/platforms v1.0.0-rc.1
github.com/davecgh/go-spew v1.1.1
github.com/distribution/reference v0.6.0
github.com/docker/buildx v0.25.0
github.com/docker/cli v28.2.2+incompatible
github.com/docker/buildx v0.28.0-rc1
github.com/docker/cli v28.3.3+incompatible
github.com/docker/cli-docs-tool v0.10.0
github.com/docker/docker v28.2.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/docker v28.3.3+incompatible
github.com/docker/go-connections v0.6.0
github.com/docker/go-units v0.5.0
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
github.com/fsnotify/fsevents v0.2.0
github.com/google/go-cmp v0.7.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.7.0
github.com/jonboulle/clockwork v0.5.0
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-ps v1.0.0
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/buildkit v0.23.0
github.com/moby/buildkit v0.24.0-rc1
github.com/moby/go-archive v0.1.0
github.com/moby/patternmatcher v0.6.0
github.com/moby/sys/atomicwriter v0.1.0
@ -41,21 +40,21 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.11.1
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
go.opentelemetry.io/otel/metric v1.35.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
go.opentelemetry.io/otel/metric v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
go.uber.org/goleak v1.3.0
go.uber.org/mock v0.5.2
golang.org/x/sync v0.15.0
golang.org/x/sys v0.33.0
google.golang.org/grpc v1.73.0
go.uber.org/mock v0.6.0
golang.org/x/sync v0.16.0
golang.org/x/sys v0.35.0
google.golang.org/grpc v1.74.2
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v1.0.1
@ -64,7 +63,7 @@ require (
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
@ -89,7 +88,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
@ -98,12 +97,12 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
@ -117,6 +116,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
@ -175,20 +175,21 @@ require (
github.com/zclconf/go-cty v1.16.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
@ -205,7 +206,7 @@ require (
)
exclude (
// FIXME(thaJeztah): remoove this once kubernetes updated their dependencies to no longer need this.
// FIXME(thaJeztah): remove this once kubernetes updated their dependencies to no longer need this.
//
// For additional details, see this PR and links mentioned in that PR:
// https://github.com/kubernetes-sigs/kustomize/pull/5830#issuecomment-2569960859

136
go.sum
View File

@ -10,8 +10,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
@ -80,16 +80,16 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.6.5 h1:H7xP5OMKdkN2p0brx01slxIU6dE/q6ybbG+jozPtIqk=
github.com/compose-spec/compose-go/v2 v2.6.5/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU=
github.com/compose-spec/compose-go/v2 v2.8.1 h1:27O4dzyhiS/UEUKp1zHOHCBWD1WbxGsYGMNNaSejTk4=
github.com/compose-spec/compose-go/v2 v2.8.1/go.mod h1:veko/VB7URrg/tKz3vmIAQDaz+CGiXH8vZsW79NmAww=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0=
github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI=
github.com/containerd/containerd/v2 v2.1.2 h1:4ZQxB+FVYmwXZgpBcKfar6ieppm3KC5C6FRKvtJ6DRU=
github.com/containerd/containerd/v2 v2.1.2/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ=
github.com/containerd/containerd/v2 v2.1.4/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@ -113,8 +113,9 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
@ -127,24 +128,24 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/buildx v0.25.0 h1:qs5WxBo0wQKSXcQ+v6UhWaeM2Pu+95ZCymaimRzInaE=
github.com/docker/buildx v0.25.0/go.mod h1:xJcOeBhz49tgqN174MMGuOU4bxNmgfaLnZn7Gm641EE=
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/buildx v0.28.0-rc1 h1:XQ0hwhIB+Jf211iBdts30WtsqIQ8/xY7/QcYDzfDFEY=
github.com/docker/buildx v0.28.0-rc1/go.mod h1:LGGGhqW6EbnC96hK9XSVh4h7kI+G/bwTgJJh/uAuQWo=
github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
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=
@ -171,8 +172,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@ -188,8 +189,8 @@ github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -317,8 +318,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z
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.23.0 h1:HV+u7xM2IZhAjVautFR2l5FNhkxFR0jhF5ILXyc3398=
github.com/moby/buildkit v0.23.0/go.mod h1:v5jMDvQgUyidk3wu3NvVAAd5JJo83nfet9Gf/o0+EAQ=
github.com/moby/buildkit v0.24.0-rc1 h1:taA+MPeYWtGyRQ1SGbdTHVk5khWKFR7f9WI2coW/Ggs=
github.com/moby/buildkit v0.24.0-rc1/go.mod h1:4qovICAdR2H4C7+EGMRva5zgHW1gyhT4/flHI7F5F9k=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
@ -450,8 +451,9 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c h1:2EejZtjFjKJGk71ANb+wtFK5EjUzUkEM3R0xnp559xg=
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -468,8 +470,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
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=
@ -508,36 +510,38 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -546,13 +550,13 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -562,10 +566,10 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -573,8 +577,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -596,19 +600,19 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -616,19 +620,19 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=

View File

@ -1,4 +1,4 @@
//go:build linux || openbsd
//go:build linux || openbsd || freebsd
/*
Copyright 2020 Docker Compose CLI authors

View File

@ -0,0 +1,38 @@
/*
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 registry
import "github.com/distribution/reference"
const (
// IndexHostname is the index hostname, used for authentication and image search.
IndexHostname = "index.docker.io"
// IndexServer is used for user auth and image search
IndexServer = "https://index.docker.io/v1/"
// IndexName is the name of the index
IndexName = "docker.io"
)
// GetAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
func GetAuthConfigKey(reposName reference.Named) string {
indexName := reference.Domain(reposName)
if indexName == IndexName || indexName == IndexHostname {
return IndexServer
}
return indexName
}

View File

@ -29,11 +29,11 @@ import (
"path"
"path/filepath"
"strings"
"github.com/hashicorp/go-multierror"
"sync"
"github.com/docker/docker/api/types/container"
"github.com/moby/go-archive"
"golang.org/x/sync/errgroup"
)
type archiveEntry struct {
@ -84,7 +84,14 @@ func (t *Tar) Sync(ctx context.Context, service string, paths []*PathMapping) er
if len(pathsToDelete) != 0 {
deleteCmd = append([]string{"rm", "-rf"}, pathsToDelete...)
}
var eg multierror.Group
var (
eg errgroup.Group
errMu sync.Mutex
errs = make([]error, 0, len(containers)*2) // max 2 errs per container
)
eg.SetLimit(16) // arbitrary limit, adjust to taste :D
for i := range containers {
containerID := containers[i].ID
tarReader := tarArchive(pathsToCopy)
@ -92,17 +99,23 @@ func (t *Tar) Sync(ctx context.Context, service string, paths []*PathMapping) er
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)
errMu.Lock()
errs = append(errs, fmt.Errorf("deleting paths in %s: %w", containerID, err))
errMu.Unlock()
}
}
if err := t.client.Untar(ctx, containerID, tarReader); err != nil {
return fmt.Errorf("copying files to %s: %w", containerID, err)
errMu.Lock()
errs = append(errs, fmt.Errorf("copying files to %s: %w", containerID, err))
errMu.Unlock()
}
return nil
return nil // don't fail-fast; collect all errors
})
}
return eg.Wait().ErrorOrNil()
_ = eg.Wait()
return errors.Join(errs...)
}
type ArchiveBuilder struct {
@ -229,7 +242,7 @@ func (a *ArchiveBuilder) writeEntry(entry archiveEntry) error {
return nil
}
// tarPath writes the given source path into tarWriter at the given dest (recursively for directories).
// entriesForPath 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) {

View File

@ -77,11 +77,13 @@ func ProjectOptions(ctx context.Context, proj *types.Project) SpanOptions {
attribute.StringSlice("project.networks", proj.NetworkNames()),
attribute.StringSlice("project.secrets", proj.SecretNames()),
attribute.StringSlice("project.configs", proj.ConfigNames()),
attribute.StringSlice("project.models", proj.ModelNames()),
attribute.StringSlice("project.extensions", keys(proj.Extensions)),
attribute.StringSlice("project.services.active", proj.ServiceNames()),
attribute.StringSlice("project.services.disabled", proj.DisabledServiceNames()),
attribute.StringSlice("project.services.build", proj.ServicesWithBuild()),
attribute.StringSlice("project.services.depends_on", proj.ServicesWithDependsOn()),
attribute.StringSlice("project.services.models", proj.ServicesWithModels()),
attribute.StringSlice("project.services.capabilities", capabilities),
attribute.StringSlice("project.services.capabilities.gpu", gpu),
attribute.StringSlice("project.services.capabilities.tpu", tpu),
@ -110,6 +112,7 @@ func ServiceOptions(service types.ServiceConfig) SpanOptions {
attribute.String("service.name", service.Name),
attribute.String("service.image", service.Image),
attribute.StringSlice("service.networks", keys(service.Networks)),
attribute.StringSlice("service.models", keys(service.Models)),
}
configNames := make([]string, len(service.Configs))

View File

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

View File

@ -18,8 +18,9 @@ package tracing
import (
"context"
"errors"
"sync"
"github.com/hashicorp/go-multierror"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
@ -28,23 +29,45 @@ type MuxExporter struct {
}
func (m MuxExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
var eg multierror.Group
for i := range m.exporters {
exporter := m.exporters[i]
eg.Go(func() error {
return exporter.ExportSpans(ctx, spans)
})
var (
wg sync.WaitGroup
errMu sync.Mutex
errs = make([]error, 0, len(m.exporters))
)
for _, exporter := range m.exporters {
wg.Add(1)
go func() {
defer wg.Done()
if err := exporter.ExportSpans(ctx, spans); err != nil {
errMu.Lock()
errs = append(errs, err)
errMu.Unlock()
}
}()
}
return eg.Wait()
wg.Wait()
return errors.Join(errs...)
}
func (m MuxExporter) Shutdown(ctx context.Context) error {
var eg multierror.Group
for i := range m.exporters {
exporter := m.exporters[i]
eg.Go(func() error {
return exporter.Shutdown(ctx)
})
var (
wg sync.WaitGroup
errMu sync.Mutex
errs = make([]error, 0, len(m.exporters))
)
for _, exporter := range m.exporters {
wg.Add(1)
go func() {
defer wg.Done()
if err := exporter.Shutdown(ctx); err != nil {
errMu.Lock()
errs = append(errs, err)
errMu.Unlock()
}
}()
}
return eg.Wait()
wg.Wait()
return errors.Join(errs...)
}

View File

@ -19,6 +19,7 @@ package api
import (
"context"
"fmt"
"io"
"slices"
"strings"
"time"
@ -26,6 +27,7 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/volume"
)
// Service manages a compose project
@ -98,8 +100,16 @@ type Service interface {
Commit(ctx context.Context, projectName string, options CommitOptions) error
// Generate generates a Compose Project from existing containers
Generate(ctx context.Context, options GenerateOptions) (*types.Project, error)
// Volumes executes the equivalent to a `docker volume ls`
Volumes(ctx context.Context, project *types.Project, options VolumesOptions) ([]VolumesSummary, error)
}
type VolumesOptions struct {
Services []string
}
type VolumesSummary = *volume.Volume
type ScaleOptions struct {
Services []string
}
@ -161,8 +171,14 @@ type BuildOptions struct {
Print bool
// Check let builder validate build configuration
Check bool
// Provenance
Provenance bool
// Attestations allows to enable attestations generation
Attestations bool
// Provenance generate a provenance attestation
Provenance string
// SBOM generate a SBOM attestation
SBOM string
// Out is the stream to write build progress
Out io.Writer
}
// Apply mutates project according to build options
@ -389,6 +405,8 @@ type AttachOptions struct {
type EventsOptions struct {
Services []string
Consumer func(event Event) error
Since string
Until string
}
// Event is a container runtime event served by Events API
@ -540,6 +558,7 @@ type ImageSummary struct {
Tag string
Platform platforms.Platform
Size int64
Created time.Time
LastTagTime time.Time
}
@ -638,7 +657,6 @@ type LogConsumer interface {
Log(containerName, message string)
Err(containerName, message string)
Status(container, msg string)
Register(container string)
}
// ContainerEventListener is a callback to process ContainerEvent from services
@ -646,16 +664,18 @@ type ContainerEventListener func(event ContainerEvent)
// ContainerEvent notify an event has been collected on source container implementing Service
type ContainerEvent struct {
Type int
// Container is the name of the container _without the project prefix_.
Type int
Time int64
Container *ContainerSummary
// Source is the name of the container _without the project prefix_.
//
// This is only suitable for display purposes within Compose, as it's
// not guaranteed to be unique across services.
Container string
ID string
Service string
Line string
// ContainerEventExit only
Source string
ID string
Service string
Line string
// ExitCode is only set on ContainerEventExited events
ExitCode int
Restarting bool
}
@ -665,17 +685,19 @@ const (
ContainerEventLog = iota
// ContainerEventErr is a ContainerEvent of type log on stderr. Line is set
ContainerEventErr
// ContainerEventAttach is a ContainerEvent of type attach. First event sent about a container
ContainerEventAttach
// ContainerEventStarted let consumer know a container has been started
ContainerEventStarted
// ContainerEventRestarted let consumer know a container has been restarted
ContainerEventRestarted
// ContainerEventStopped is a ContainerEvent of type stopped.
ContainerEventStopped
// ContainerEventCreated let consumer know a new container has been created
ContainerEventCreated
// ContainerEventRecreated let consumer know container stopped but his being replaced
ContainerEventRecreated
// ContainerEventExit is a ContainerEvent of type exit. ExitCode is set
ContainerEventExit
// ContainerEventExited is a ContainerEvent of type exit. ExitCode is set
ContainerEventExited
// UserCancel user cancelled compose up, we are stopping containers
UserCancel
// HookEventLog is a ContainerEvent of type log on stdout by service hook
HookEventLog
)

View File

@ -130,7 +130,7 @@ func (d *DryRunClient) ContainerInspect(ctx context.Context, container string) (
ID: id,
Name: container,
State: &containerType.State{
Status: "running", // needed for --wait option
Status: containerType.StateRunning, // needed for --wait option
Health: &containerType.Health{
Status: containerType.Healthy, // needed for healthcheck control
},

View File

@ -20,6 +20,7 @@ import (
"github.com/docker/cli/cli/streams"
)
// Streams defines the standard streams (stdin, stdout, stderr) used by the CLI.
type Streams interface {
Out() *streams.Out
Err() *streams.Out

View File

@ -17,8 +17,6 @@
package api
import (
"fmt"
"github.com/hashicorp/go-version"
"github.com/docker/compose/v2/internal"
@ -65,9 +63,6 @@ var ComposeVersion string
func init() {
v, err := version.NewVersion(internal.Version)
if err == nil {
segments := v.Segments()
if len(segments) > 2 {
ComposeVersion = fmt.Sprintf("%d.%d.%d", segments[0], segments[1], segments[2])
}
ComposeVersion = v.Core().String()
}
}

35
pkg/api/labels_test.go Normal file
View File

@ -0,0 +1,35 @@
/*
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 api
import (
"testing"
"github.com/docker/compose/v2/internal"
"github.com/hashicorp/go-version"
"gotest.tools/v3/assert"
)
func TestComposeVersionInitialization(t *testing.T) {
v, err := version.NewVersion(internal.Version)
if err != nil {
assert.Equal(t, "", ComposeVersion, "ComposeVersion should be empty for a non-semver internal version (e.g. 'devel')")
} else {
expected := v.Core().String()
assert.Equal(t, expected, ComposeVersion, "ComposeVersion should be the core of internal.Version")
}
}

View File

@ -23,10 +23,11 @@ import (
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"github.com/compose-spec/compose-go/v2/types"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli/command"
cli "github.com/docker/cli/cli/command/container"
"github.com/docker/compose/v2/pkg/api"
@ -112,15 +113,20 @@ func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, o
return err
}
usr, err := user.Current()
if err != nil {
return err
}
created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
containerConfig := &container.Config{
Image: transformation,
Env: []string{"LICENSE_AGREEMENT=true"},
User: usr.Uid,
}, &container.HostConfig{
}
// On POSIX systems, this is a decimal number representing the uid.
// On Windows, this is a security identifier (SID) in a string format and the engine isn't able to manage it
if runtime.GOOS != "windows" {
usr, err := user.Current()
if err != nil {
return err
}
containerConfig.User = usr.Uid
}
created, err := dockerCli.Client().ContainerCreate(ctx, containerConfig, &container.HostConfig{
AutoRemove: true,
Binds: binds,
}, &network.NetworkingConfig{}, nil, "")
@ -198,7 +204,7 @@ func loadFileObject(conf types.FileObjectConfig) (types.FileObjectConfig, error)
func inspectWithPull(ctx context.Context, dockerCli command.Cli, imageName string) (image.InspectResponse, error) {
inspect, err := dockerCli.Client().ImageInspect(ctx, imageName)
if cerrdefs.IsNotFound(err) {
if errdefs.IsNotFound(err) {
var stream io.ReadCloser
stream, err = dockerCli.Client().ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {

View File

@ -33,6 +33,8 @@ import (
const (
TransformerLabel = "com.docker.compose.bridge"
DefaultTransformerImage = "docker/compose-bridge-kubernetes"
templatesPath = "/templates"
)
type CreateTransformerOptions struct {
@ -73,7 +75,7 @@ func CreateTransformer(ctx context.Context, dockerCli command.Cli, options Creat
if err != nil {
return err
}
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, "/templates")
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, templatesPath)
if err != nil {
return err
}
@ -82,7 +84,7 @@ func CreateTransformer(ctx context.Context, dockerCli command.Cli, options Creat
}()
srcInfo := archive.CopyInfo{
Path: "/templates",
Path: templatesPath,
Exists: true,
IsDir: stat.Mode.IsDir(),
}

View File

@ -18,8 +18,8 @@ package compose
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/config/configfile"
@ -41,11 +41,9 @@ func (s *composeService) useAPISocket(project *types.Project) (*types.Project, e
return project, nil
}
socket := s.dockerCli.DockerEndpoint().Host
if !strings.HasPrefix(socket, "unix://") {
return nil, fmt.Errorf("use_api_socket can only be used with unix sockets: docker endpoint %s is incompatible", socket)
if s.dockerCli.ServerInfo().OSType == "windows" {
return nil, errors.New("use_api_socket can't be used with a Windows Docker Engine")
}
socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
creds, err := s.dockerCli.ConfigFile().GetAllCredentials()
if err != nil {
@ -64,9 +62,12 @@ func (s *composeService) useAPISocket(project *types.Project) (*types.Project, e
}
for name, service := range project.Services {
if !service.UseAPISocket {
continue
}
service.Volumes = append(service.Volumes, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind,
Source: socket,
Source: "/var/run/docker.sock",
Target: "/var/run/docker.sock",
})

View File

@ -61,41 +61,37 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis
}
func (s *composeService) attachContainer(ctx context.Context, container containerType.Summary, listener api.ContainerEventListener) error {
serviceName := container.Labels[api.ServiceLabel]
containerName := getContainerNameWithoutProject(container)
service := container.Labels[api.ServiceLabel]
name := getContainerNameWithoutProject(container)
return s.doAttachContainer(ctx, service, container.ID, name, listener)
}
listener(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: containerName,
ID: container.ID,
Service: serviceName,
})
wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventLog,
Container: containerName,
ID: container.ID,
Service: serviceName,
Line: line,
})
})
wErr := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventErr,
Container: containerName,
ID: container.ID,
Service: serviceName,
Line: line,
})
})
inspect, err := s.apiClient().ContainerInspect(ctx, container.ID)
func (s *composeService) doAttachContainer(ctx context.Context, service, id, name string, listener api.ContainerEventListener) error {
inspect, err := s.apiClient().ContainerInspect(ctx, id)
if err != nil {
return err
}
_, _, err = s.attachContainerStreams(ctx, container.ID, inspect.Config.Tty, nil, wOut, wErr)
wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventLog,
Source: name,
ID: id,
Service: service,
Line: line,
})
})
wErr := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.ContainerEventErr,
Source: name,
ID: id,
Service: service,
Line: line,
})
})
_, _, err = s.attachContainerStreams(ctx, id, inspect.Config.Tty, nil, wOut, wErr)
return err
}

View File

@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
@ -172,7 +173,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
if options.Quiet {
options.Progress = progress.ModeQuiet
}
if options.Progress == "" {
if options.Progress == progress.ModeAuto {
options.Progress = os.Getenv("BUILDKIT_PROGRESS")
}
w, err = xprogress.NewPrinter(progressCtx, os.Stdout, progressui.DisplayMode(options.Progress),
@ -397,6 +398,7 @@ func resolveAndMergeBuildArgs(dockerCli command.Cli, project *types.Project, ser
return result
}
//nolint:gocyclo
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
plats, err := parsePlatforms(service)
if err != nil {
@ -471,8 +473,19 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
}
attests := map[string]*string{}
if !options.Provenance {
attests["provenance"] = nil
if options.Attestations {
if service.Build.Provenance != "" {
attests["provenance"] = attestation(service.Build.Provenance, "provenance")
}
if service.Build.SBOM != "" {
attests["sbom"] = attestation(service.Build.SBOM, "sbom")
}
}
if options.Provenance != "" {
attests["provenance"] = attestation(options.Provenance, "provenance")
}
if options.SBOM != "" {
attests["sbom"] = attestation(options.SBOM, "sbom")
}
return build.Options{
@ -502,6 +515,16 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
}, nil
}
func attestation(attest string, val string) *string {
if b, err := strconv.ParseBool(val); err == nil {
s := fmt.Sprintf("type=%s,disabled=%t", attest, b)
return &s
} else {
s := fmt.Sprintf("type=%s,%s", attest, val)
return &s
}
}
func toUlimitOpt(ulimits map[string]*types.UlimitsConfig) *cliopts.UlimitOpt {
ref := map[string]*container.Ulimit{}
for _, limit := range toUlimits(ulimits) {

View File

@ -20,9 +20,11 @@ import (
"bufio"
"bytes"
"context"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"os"
"os/exec"
@ -32,20 +34,18 @@ import (
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/builder/remotecontext/urlutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/util/gitutil"
gitutil "github.com/moby/buildkit/frontend/dockerfile/dfgitutil"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"golang.org/x/sync/errgroup"
)
@ -59,6 +59,9 @@ func buildWithBake(dockerCli command.Cli) (bool, error) {
return false, err
}
if !bake {
if ok {
logrus.Warnf("COMPOSE_BAKE=false is deprecated, support for internal compose builder will be removed in next release")
}
return false, nil
}
@ -73,7 +76,7 @@ func buildWithBake(dockerCli command.Cli) (bool, error) {
_, err = manager.GetPlugin("buildx", dockerCli, &cobra.Command{})
if err != nil {
if manager.IsNotFound(err) {
if errdefs.IsNotFound(err) {
logrus.Warnf("Docker Compose is configured to build using Bake, but buildx isn't installed")
return false, nil
}
@ -116,6 +119,7 @@ type bakeTarget struct {
Entitlements []string `json:"entitlements,omitempty"`
ExtraHosts map[string]string `json:"extra-hosts,omitempty"`
Outputs []string `json:"output,omitempty"`
Attest []string `json:"attest,omitempty"`
}
type bakeMetadata map[string]buildStatus
@ -128,7 +132,18 @@ type buildStatus struct {
func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
eg := errgroup.Group{}
ch := make(chan *client.SolveStatus)
display, err := progressui.NewDisplay(os.Stdout, progressui.DisplayMode(options.Progress))
if options.Progress == progress.ModeAuto {
options.Progress = os.Getenv("BUILDKIT_PROGRESS")
}
displayMode := progressui.DisplayMode(options.Progress)
out := options.Out
if out == nil {
if !s.dockerCli.Out().IsTerminal() {
displayMode = progressui.PlainMode
}
out = os.Stdout // should be s.dockerCli.Out(), but NewDisplay require access to the underlying *File
}
display, err := progressui.NewDisplay(out, displayMode)
if err != nil {
return nil, err
}
@ -142,10 +157,11 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
Targets: map[string]bakeTarget{},
}
var (
group bakeGroup
privileged bool
read []string
targets = make(map[string]string, len(serviceToBeBuild)) // service name -> build target
group bakeGroup
privileged bool
read []string
expectedImages = make(map[string]string, len(serviceToBeBuild)) // service name -> expected image
targets = make(map[string]string, len(serviceToBeBuild)) // service name -> build target
)
// produce a unique ID for service used as bake target
@ -165,6 +181,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
continue
}
build := *service.Build
labels := getImageBuildLabels(project, service)
args := types.Mapping{}
for k, v := range resolveAndMergeBuildArgs(s.dockerCli, project, service, options) {
@ -192,7 +209,11 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
case len(service.Build.Platforms) > 1:
outputs = []string{fmt.Sprintf("type=image,push=%t", push)}
default:
outputs = []string{fmt.Sprintf("type=docker,load=true,push=%t", push)}
if push {
outputs = []string{"type=registry"}
} else {
outputs = []string{"type=docker"}
}
}
read = append(read, build.Context)
@ -203,6 +224,12 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
}
}
image := api.GetImageNameOrDefault(service, project.Name)
expectedImages[serviceName] = image
pull := service.Build.Pull || options.Pull
noCache := service.Build.NoCache || options.NoCache
target := targets[serviceName]
cfg.Targets[target] = bakeTarget{
Context: build.Context,
@ -210,17 +237,18 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
Dockerfile: dockerFilePath(build.Context, build.Dockerfile),
DockerfileInline: strings.ReplaceAll(build.DockerfileInline, "${", "$${"),
Args: args,
Labels: build.Labels,
Tags: append(build.Tags, api.GetImageNameOrDefault(service, project.Name)),
Labels: labels,
Tags: append(build.Tags, image),
CacheFrom: build.CacheFrom,
// CacheTo: TODO
CacheFrom: build.CacheFrom,
CacheTo: build.CacheTo,
NetworkMode: build.Network,
Platforms: build.Platforms,
Target: build.Target,
Secrets: toBakeSecrets(project, build.Secrets),
SSH: toBakeSSH(append(build.SSH, options.SSHs...)),
Pull: options.Pull,
NoCache: options.NoCache,
Pull: pull,
NoCache: noCache,
ShmSize: build.ShmSize,
Ulimits: toBakeUlimits(build.Ulimits),
Entitlements: entitlements,
@ -228,6 +256,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
Outputs: outputs,
Call: call,
Attest: toBakeAttest(build),
}
}
@ -281,6 +310,12 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
args = append(args, "--allow", "security.insecure")
}
}
if options.SBOM != "" {
args = append(args, "--sbom="+options.SBOM)
}
if options.Provenance != "" {
args = append(args, "--provenance="+options.Provenance)
}
if options.Builder != "" {
args = append(args, "--builder", options.Builder)
@ -291,23 +326,15 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
logrus.Debugf("Executing bake with args: %v", args)
cmd := exec.CommandContext(ctx, buildx.Path, args...)
// Remove DOCKER_CLI_PLUGIN... variable so buildx can detect it run standalone
cmd.Env = filter(os.Environ(), manager.ReexecEnvvar)
// Use docker/cli mechanism to propagate termination signal to child process
server, err := socket.NewPluginServer(nil)
if err == nil {
defer server.Close() //nolint:errcheck
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
if s.dryRun {
return dryRunBake(ctx, cfg), nil
}
cmd := exec.CommandContext(ctx, buildx.Path, args...)
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, &carrier)
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
err = s.prepareShellOut(ctx, project.Environment, cmd)
if err != nil {
return nil, err
}
cmd.Stdout = s.stdout()
cmd.Stdin = bytes.NewBuffer(b)
@ -317,16 +344,22 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
}
var errMessage []string
scanner := bufio.NewScanner(pipe)
scanner.Split(bufio.ScanLines)
reader := bufio.NewReader(pipe)
err = cmd.Start()
if err != nil {
return nil, err
}
eg.Go(cmd.Wait)
for scanner.Scan() {
line := scanner.Text()
for {
line, readErr := reader.ReadString('\n')
if readErr != nil {
if readErr == io.EOF {
break
} else {
return nil, fmt.Errorf("failed to execute bake: %w", readErr)
}
}
decoder := json.NewDecoder(strings.NewReader(line))
var status client.SolveStatus
err := decoder.Decode(&status)
@ -364,13 +397,14 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
cw := progress.ContextWriter(ctx)
results := map[string]string{}
for name := range serviceToBeBuild {
image := expectedImages[name]
target := targets[name]
built, ok := md[target]
if !ok {
return nil, fmt.Errorf("build result not found in Bake metadata for service %s", name)
}
results[name] = built.Digest
cw.Event(progress.BuiltEvent(name))
results[image] = built.Digest
cw.Event(progress.BuiltEvent(image))
}
return results, nil
}
@ -432,20 +466,28 @@ func toBakeSecrets(project *types.Project, secrets []types.ServiceSecretConfig)
return s
}
func filter(environ []string, variable string) []string {
prefix := variable + "="
filtered := make([]string, 0, len(environ))
for _, val := range environ {
if !strings.HasPrefix(val, prefix) {
filtered = append(filtered, val)
func toBakeAttest(build types.BuildConfig) []string {
var attests []string
// Handle per-service provenance configuration (only from build config, not global options)
if build.Provenance != "" {
if build.Provenance == "true" {
attests = append(attests, "type=provenance")
} else if build.Provenance != "false" {
attests = append(attests, fmt.Sprintf("type=provenance,%s", build.Provenance))
}
}
return filtered
}
func replace(environ []string, variable, value string) []string {
filtered := filter(environ, variable)
return append(filtered, fmt.Sprintf("%s=%s", variable, value))
// Handle per-service SBOM configuration (only from build config, not global options)
if build.SBOM != "" {
if build.SBOM == "true" {
attests = append(attests, "type=sbom")
} else if build.SBOM != "false" {
attests = append(attests, fmt.Sprintf("type=sbom,%s", build.SBOM))
}
}
return attests
}
func dockerFilePath(ctxName string, dockerfile string) string {
@ -458,9 +500,37 @@ func dockerFilePath(ctxName string, dockerfile string) string {
if !filepath.IsAbs(dockerfile) {
dockerfile = filepath.Join(ctxName, dockerfile)
}
symlinks, err := filepath.EvalSymlinks(dockerfile)
dir := filepath.Dir(dockerfile)
symlinks, err := filepath.EvalSymlinks(dir)
if err == nil {
return symlinks
return filepath.Join(symlinks, filepath.Base(dockerfile))
}
return dockerfile
}
func dryRunBake(ctx context.Context, cfg bakeConfig) map[string]string {
w := progress.ContextWriter(ctx)
bakeResponse := map[string]string{}
for name, target := range cfg.Targets {
dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name)))
displayDryRunBuildEvent(w, name, dryRunUUID, target.Tags[0])
bakeResponse[name] = dryRunUUID
}
for name := range bakeResponse {
w.Event(progress.BuiltEvent(name))
}
return bakeResponse
}
func displayDryRunBuildEvent(w progress.Writer, name string, dryRunUUID, tag string) {
w.Event(progress.Event{
ID: name + " ==>",
Status: progress.Done,
Text: fmt.Sprintf("==> writing image %s", dryRunUUID),
})
w.Event(progress.Event{
ID: name + " ==> ==>",
Status: progress.Done,
Text: fmt.Sprintf(`naming to %s`, tag),
})
}

View File

@ -70,16 +70,7 @@ func (s composeService) dryRunBuildResponse(ctx context.Context, name string, op
w := progress.ContextWriter(ctx)
buildResponse := map[string]*client.SolveResponse{}
dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name)))
w.Event(progress.Event{
ID: "==>",
Status: progress.Done,
Text: fmt.Sprintf("==> writing image %s", dryRunUUID),
})
w.Event(progress.Event{
ID: "==> ==>",
Status: progress.Done,
Text: fmt.Sprintf(`naming to %s`, options.Tags[0]),
})
displayDryRunBuildEvent(w, name, dryRunUUID, options.Tags[0])
buildResponse[name] = &client.SolveResponse{ExporterResponse: map[string]string{
"containerimage.digest": dryRunUUID,
}}

View File

@ -128,12 +128,6 @@ func isService(services ...string) containerPredicate {
}
}
func isRunning() containerPredicate {
return func(c container.Summary) bool {
return c.State == "running"
}
}
// isOrphaned is a predicate to select containers without a matching service definition in compose project
func isOrphaned(project *types.Project) containerPredicate {
services := append(project.ServiceNames(), project.DisabledServiceNames()...)

View File

@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"maps"
"slices"
"sort"
"strconv"
@ -236,7 +237,7 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
err := c.service.stop(ctx, project.Name, api.StopOptions{
Services: dependents,
Project: project,
})
}, nil)
if err != nil {
return err
}
@ -563,6 +564,9 @@ func shouldWaitForDependency(serviceName string, dependencyConfig types.ServiceD
} else if service.GetScale() == 0 {
// don't wait for the dependency which configured to have 0 containers running
return false, nil
} else if service.Provider != nil {
// don't wait for provider services
return false, nil
}
return true, nil
}
@ -632,13 +636,18 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
if inherit {
inherited = &replaced
}
replacedContainerName := service.ContainerName
if replacedContainerName == "" {
replacedContainerName = service.Name + api.Separator + strconv.Itoa(number)
}
name := getContainerName(project.Name, service, number)
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
opts := createOptions{
AutoRemove: false,
AttachStdin: false,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID),
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replacedContainerName),
}
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts, w)
if err != nil {
@ -656,7 +665,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
return created, err
}
err = s.apiClient().ContainerRename(ctx, created.ID, name)
err = s.apiClient().ContainerRename(ctx, tmpName, name)
if err != nil {
return created, err
}
@ -756,14 +765,7 @@ func (s *composeService) createMobyContainer(ctx context.Context,
}
}
}
err = s.injectSecrets(ctx, project, service, created.ID)
if err != nil {
return created, err
}
err = s.injectConfigs(ctx, project, service, created.ID)
return created, err
return created, nil
}
// getLinks mimics V1 compose/service.py::Service::_get_links()
@ -826,25 +828,25 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) {
for _, c := range containers {
container, err := s.apiClient().ContainerInspect(ctx, c.ID)
ctr, err := s.apiClient().ContainerInspect(ctx, c.ID)
if err != nil {
return false, err
}
name := container.Name[1:]
name := ctr.Name[1:]
if container.State.Status == "exited" {
return false, fmt.Errorf("container %s exited (%d)", name, container.State.ExitCode)
if ctr.State.Status == containerType.StateExited {
return false, fmt.Errorf("container %s exited (%d)", name, ctr.State.ExitCode)
}
if container.Config.Healthcheck == nil && fallbackRunning {
if ctr.Config.Healthcheck == nil && fallbackRunning {
// Container does not define a health check, but we can fall back to "running" state
return container.State != nil && container.State.Status == "running", nil
return ctr.State != nil && ctr.State.Status == containerType.StateRunning, nil
}
if container.State == nil || container.State.Health == nil {
if ctr.State == nil || ctr.State.Health == nil {
return false, fmt.Errorf("container %s has no healthcheck configured", name)
}
switch container.State.Health.Status {
switch ctr.State.Health.Status {
case containerType.Healthy:
// Continue by checking the next container.
case containerType.Unhealthy:
@ -852,7 +854,7 @@ func (s *composeService) isServiceHealthy(ctx context.Context, containers Contai
case containerType.Starting:
return false, nil
default:
return false, fmt.Errorf("container %s had unexpected health status %q", name, container.State.Health.Status)
return false, fmt.Errorf("container %s had unexpected health status %q", name, ctr.State.Health.Status)
}
}
return true, nil
@ -860,12 +862,12 @@ func (s *composeService) isServiceHealthy(ctx context.Context, containers Contai
func (s *composeService) isServiceCompleted(ctx context.Context, containers Containers) (bool, int, error) {
for _, c := range containers {
container, err := s.apiClient().ContainerInspect(ctx, c.ID)
ctr, err := s.apiClient().ContainerInspect(ctx, c.ID)
if err != nil {
return false, 0, err
}
if container.State != nil && container.State.Status == "exited" {
return true, container.State.ExitCode, nil
if ctr.State != nil && ctr.State.Status == containerType.StateExited {
return true, ctr.State.ExitCode, nil
}
}
return false, 0, nil
@ -897,6 +899,17 @@ func (s *composeService) startService(ctx context.Context,
if ctr.State == ContainerRunning {
continue
}
err = s.injectSecrets(ctx, project, service, ctr.ID)
if err != nil {
return err
}
err = s.injectConfigs(ctx, project, service, ctr.ID)
if err != nil {
return err
}
eventName := getContainerProgressName(ctr)
w.Event(progress.StartingEvent(eventName))
err = s.apiClient().ContainerStart(ctx, ctr.ID, containerType.StartOptions{})
@ -919,9 +932,7 @@ func (s *composeService) startService(ctx context.Context,
func mergeLabels(ls ...types.Labels) types.Labels {
merged := types.Labels{}
for _, l := range ls {
for k, v := range l {
merged[k] = v
}
maps.Copy(merged, l)
}
return merged
}

View File

@ -31,7 +31,6 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/system"
"github.com/moby/go-archive"
)
@ -161,7 +160,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
// If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
linkTarget := dstStat.LinkTarget
if !system.IsAbs(linkTarget) {
if !isAbs(linkTarget) {
// Join with the parent directory.
dstParent, _ := archive.SplitPathDirEntry(dstPath)
linkTarget = filepath.Join(dstParent, linkTarget)
@ -264,7 +263,7 @@ func (s *composeService) copyFromContainer(ctx context.Context, containerID, src
// If the destination is a symbolic link, we should follow it.
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
linkTarget := srcStat.LinkTarget
if !system.IsAbs(linkTarget) {
if !isAbs(linkTarget) {
// Join with the parent directory.
srcParent, _ := archive.SplitPathDirEntry(srcPath)
linkTarget = filepath.Join(srcParent, linkTarget)
@ -302,8 +301,20 @@ func (s *composeService) copyFromContainer(ctx context.Context, containerID, src
return archive.CopyTo(preArchive, srcInfo, dstPath)
}
// IsAbs is a platform-agnostic wrapper for filepath.IsAbs.
//
// On Windows, golang filepath.IsAbs does not consider a path \windows\system32
// as absolute as it doesn't start with a drive-letter/colon combination. However,
// in docker we need to verify things such as WORKDIR /windows/system32 in
// a Dockerfile (which gets translated to \windows\system32 when being processed
// by the daemon). This SHOULD be treated as absolute from a docker processing
// perspective.
func isAbs(path string) bool {
return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator))
}
func splitCpArg(arg string) (ctr, path string) {
if system.IsAbs(arg) {
if isAbs(arg) {
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
return "", arg
}

View File

@ -30,13 +30,12 @@ import (
"github.com/compose-spec/compose-go/v2/paths"
"github.com/compose-spec/compose-go/v2/types"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/errdefs"
"github.com/docker/docker/api/types/blkiodev"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/versions"
volumetypes "github.com/docker/docker/api/types/volume"
"github.com/docker/go-connections/nat"
@ -83,6 +82,11 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
return err
}
err = s.ensureModels(ctx, project, options.QuietPull)
if err != nil {
return err
}
prepareNetworks(project)
networks, err := s.ensureNetworks(ctx, project)
@ -176,15 +180,12 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
return createConfigs{}, err
}
var (
runCmd strslice.StrSlice
entrypoint strslice.StrSlice
)
var runCmd, entrypoint []string
if service.Command != nil {
runCmd = strslice.StrSlice(service.Command)
runCmd = service.Command
}
if service.Entrypoint != nil {
entrypoint = strslice.StrSlice(service.Entrypoint)
entrypoint = service.Entrypoint
}
var (
@ -281,8 +282,8 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
Annotations: service.Annotations,
Binds: binds,
Mounts: mounts,
CapAdd: strslice.StrSlice(service.CapAdd),
CapDrop: strslice.StrSlice(service.CapDrop),
CapAdd: service.CapAdd,
CapDrop: service.CapDrop,
NetworkMode: networkMode,
Init: service.Init,
IpcMode: container.IpcMode(service.Ipc),
@ -1261,7 +1262,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
}
id, err := s.resolveOrCreateNetwork(ctx, project, name, n)
if cerrdefs.IsConflict(err) {
if errdefs.IsConflict(err) {
// Maybe another execution of `docker compose up|run` created same network
// let's retry once
return s.resolveOrCreateNetwork(ctx, project, name, n)
@ -1427,7 +1428,7 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
err := s.stop(ctx, project.Name, api.StopOptions{
Services: services,
Project: project,
})
}, nil)
if err != nil {
return nil, err
}
@ -1496,7 +1497,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
sn, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
if err == nil {
networks = append(networks, sn)
} else if !cerrdefs.IsNotFound(err) {
} else if !errdefs.IsNotFound(err) {
return "", err
}
@ -1533,7 +1534,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project, assumeYes bool) (string, error) {
inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
if err != nil {
if !cerrdefs.IsNotFound(err) {
if !errdefs.IsNotFound(err) {
return "", err
}
if volume.External {
@ -1598,7 +1599,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
err := s.stop(ctx, project.Name, api.StopOptions{
Services: services,
Project: project,
})
}, nil)
if err != nil {
return err
}
@ -1622,7 +1623,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
}
func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {
eventName := fmt.Sprintf("Volume %q", volume.Name)
eventName := fmt.Sprintf("Volume %s", volume.Name)
w := progress.ContextWriter(ctx)
w.Event(progress.CreatingEvent(eventName))
hash, err := VolumeHash(volume)

View File

@ -23,7 +23,7 @@ import (
"time"
"github.com/compose-spec/compose-go/v2/types"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/errdefs"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
@ -219,7 +219,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
continue
}
nw, err := s.apiClient().NetworkInspect(ctx, net.ID, network.InspectOptions{})
if cerrdefs.IsNotFound(err) {
if errdefs.IsNotFound(err) {
w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
return nil
}
@ -233,7 +233,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
}
if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
if cerrdefs.IsNotFound(err) {
if errdefs.IsNotFound(err) {
continue
}
w.Event(progress.ErrorEvent(eventName))
@ -261,11 +261,11 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre
w.Event(progress.NewEvent(id, progress.Done, "Removed"))
return nil
}
if cerrdefs.IsConflict(err) {
if errdefs.IsConflict(err) {
w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
return nil
}
if cerrdefs.IsNotFound(err) {
if errdefs.IsNotFound(err) {
w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
return nil
}
@ -276,7 +276,7 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
resource := fmt.Sprintf("Volume %s", id)
_, err := s.apiClient().VolumeInspect(ctx, id)
if cerrdefs.IsNotFound(err) {
if errdefs.IsNotFound(err) {
// Already gone
return nil
}
@ -287,25 +287,33 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
return nil
}
if cerrdefs.IsConflict(err) {
if errdefs.IsConflict(err) {
w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
return nil
}
if cerrdefs.IsNotFound(err) {
if errdefs.IsNotFound(err) {
w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
return nil
}
return err
}
func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, service *types.ServiceConfig, ctr containerType.Summary, timeout *time.Duration) error {
func (s *composeService) stopContainer(
ctx context.Context, w progress.Writer,
service *types.ServiceConfig, ctr containerType.Summary,
timeout *time.Duration, listener api.ContainerEventListener,
) error {
eventName := getContainerProgressName(ctr)
w.Event(progress.StoppingEvent(eventName))
if service != nil {
for _, hook := range service.PreStop {
err := s.runHook(ctx, ctr, *service, hook, nil)
err := s.runHook(ctx, ctr, *service, hook, listener)
if err != nil {
// Ignore errors indicating that some containers were already stopped or removed.
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) {
return nil
}
return err
}
}
@ -321,11 +329,15 @@ func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, s
return nil
}
func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, serv *types.ServiceConfig, containers []containerType.Summary, timeout *time.Duration) error {
func (s *composeService) stopContainers(
ctx context.Context, w progress.Writer,
serv *types.ServiceConfig, containers []containerType.Summary,
timeout *time.Duration, listener api.ContainerEventListener,
) error {
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers {
eg.Go(func() error {
return s.stopContainer(ctx, w, serv, ctr, timeout)
return s.stopContainer(ctx, w, serv, ctr, timeout, listener)
})
}
return eg.Wait()
@ -344,8 +356,8 @@ func (s *composeService) removeContainers(ctx context.Context, containers []cont
func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr containerType.Summary, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
w := progress.ContextWriter(ctx)
eventName := getContainerProgressName(ctr)
err := s.stopContainer(ctx, w, service, ctr, timeout)
if cerrdefs.IsNotFound(err) {
err := s.stopContainer(ctx, w, service, ctr, timeout, nil)
if errdefs.IsNotFound(err) {
w.Event(progress.RemovedEvent(eventName))
return nil
}
@ -357,7 +369,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
Force: true,
RemoveVolumes: volumes,
})
if err != nil && !cerrdefs.IsNotFound(err) && !cerrdefs.IsConflict(err) {
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
return err
}

View File

@ -24,13 +24,13 @@ import (
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/errdefs"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
@ -326,7 +326,7 @@ func TestDownRemoveImages(t *testing.T) {
if exists {
resp.RepoTags = []string{img}
} else {
err = errdefs.NotFound(fmt.Errorf("test specified that image %q should not exist", img))
err = errdefs.ErrNotFound.WithMessage(fmt.Sprintf("test specified that image %q should not exist", img))
}
api.EXPECT().ImageInspect(gomock.Any(), img).

View File

@ -32,6 +32,8 @@ func (s *composeService) Events(ctx context.Context, projectName string, options
projectName = strings.ToLower(projectName)
evts, errors := s.apiClient().Events(ctx, events.ListOptions{
Filters: filters.NewArgs(projectFilter(projectName)),
Since: options.Since,
Until: options.Until,
})
for {
select {

View File

@ -52,7 +52,7 @@ func (s *composeService) Exec(ctx context.Context, projectName string, options a
err = container.RunExec(ctx, s.dockerCli, target.ID, exec)
var sterr cli.StatusError
if errors.As(err, &sterr) {
return sterr.StatusCode, nil
return sterr.StatusCode, err
}
return 0, err
}

View File

@ -32,11 +32,11 @@ import (
func (s composeService) runHook(ctx context.Context, ctr container.Summary, service types.ServiceConfig, hook types.ServiceHook, listener api.ContainerEventListener) error {
wOut := utils.GetWriter(func(line string) {
listener(api.ContainerEvent{
Type: api.HookEventLog,
Container: getContainerNameWithoutProject(ctr) + " ->",
ID: ctr.ID,
Service: service.Name,
Line: line,
Type: api.HookEventLog,
Source: getContainerNameWithoutProject(ctr) + " ->",
ID: ctr.ID,
Service: service.Name,
Line: line,
})
})
defer wOut.Close() //nolint:errcheck
@ -48,7 +48,6 @@ func (s composeService) runHook(ctx context.Context, ctr container.Summary, serv
Env: ToMobyEnv(hook.Environment),
WorkingDir: hook.WorkingDir,
Cmd: hook.Command,
Detach: detached,
AttachStdout: !detached,
AttachStderr: !detached,
})

View File

@ -23,7 +23,7 @@ import (
"sync"
"github.com/compose-spec/compose-go/v2/types"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/errdefs"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
@ -204,7 +204,7 @@ func (p *ImagePruner) filterImagesByExistence(ctx context.Context, imageNames []
for _, img := range imageNames {
eg.Go(func() error {
_, err := p.client.ImageInspect(ctx, img)
if cerrdefs.IsNotFound(err) {
if errdefs.IsNotFound(err) {
// err on the side of caution: only skip if we successfully
// queried the API and got back a definitive "not exists"
return nil

View File

@ -22,8 +22,9 @@ import (
"slices"
"strings"
"sync"
"time"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/container"
@ -90,6 +91,11 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
}
}
created, err := time.Parse(time.RFC3339Nano, image.Created)
if err != nil {
return err
}
mux.Lock()
defer mux.Unlock()
summary[getCanonicalContainerName(c)] = api.ImageSummary{
@ -103,6 +109,7 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
Variant: image.Variant,
},
Size: image.Size,
Created: created,
LastTagTime: image.Metadata.LastTagTime,
}
return nil
@ -121,7 +128,7 @@ func (s *composeService) getImageSummaries(ctx context.Context, repoTags []strin
eg.Go(func() error {
inspect, err := s.apiClient().ImageInspect(ctx, repoTag)
if err != nil {
if cerrdefs.IsNotFound(err) {
if errdefs.IsNotFound(err) {
return nil
}
return fmt.Errorf("unable to get image '%s': %w", repoTag, err)

View File

@ -20,6 +20,7 @@ import (
"context"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
@ -44,8 +45,12 @@ func TestImages(t *testing.T) {
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)))
listOpts := container.ListOptions{All: true, Filters: args}
api.EXPECT().ServerVersion(gomock.Any()).Return(types.Version{APIVersion: "1.96"}, nil).AnyTimes()
image1 := imageInspect("image1", "foo:1", 12345)
image2 := imageInspect("image2", "bar:2", 67890)
timeStr1 := "2025-06-06T06:06:06.000000000Z"
created1, _ := time.Parse(time.RFC3339Nano, timeStr1)
timeStr2 := "2025-03-03T03:03:03.000000000Z"
created2, _ := time.Parse(time.RFC3339Nano, timeStr2)
image1 := imageInspect("image1", "foo:1", 12345, timeStr1)
image2 := imageInspect("image2", "bar:2", 67890, timeStr2)
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(image1, nil).MaxTimes(2)
api.EXPECT().ImageInspect(anyCancellableContext(), "bar:2").Return(image2, nil)
c1 := containerDetail("service1", "123", "running", "foo:1")
@ -62,32 +67,36 @@ func TestImages(t *testing.T) {
Repository: "foo",
Tag: "1",
Size: 12345,
Created: created1,
},
"456": {
ID: "image2",
Repository: "bar",
Tag: "2",
Size: 67890,
Created: created2,
},
"789": {
ID: "image1",
Repository: "foo",
Tag: "1",
Size: 12345,
Created: created1,
},
}
assert.NilError(t, err)
assert.DeepEqual(t, images, expected)
}
func imageInspect(id string, imageReference string, size int64) image.InspectResponse {
func imageInspect(id string, imageReference string, size int64, created string) image.InspectResponse {
return image.InspectResponse{
ID: id,
RepoTags: []string{
"someRepo:someTag",
imageReference,
},
Size: size,
Size: size,
Created: created,
}
}

View File

@ -18,12 +18,10 @@ package compose
import (
"context"
"errors"
"io"
"time"
"github.com/containerd/errdefs"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/stdcopy"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
@ -63,9 +61,8 @@ func (s *composeService) Logs(
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers {
eg.Go(func() error {
err := s.logContainers(ctx, consumer, ctr, options)
var notImplErr errdefs.ErrNotImplemented
if errors.As(err, &notImplErr) {
err := s.logContainer(ctx, consumer, ctr, options)
if errdefs.IsNotImplemented(err) {
logrus.Warnf("Can't retrieve logs for %q: %s", getCanonicalContainerName(ctr), err.Error())
return nil
}
@ -74,70 +71,57 @@ func (s *composeService) Logs(
}
if options.Follow {
containers = containers.filter(isRunning())
printer := newLogPrinter(consumer)
eg.Go(func() error {
_, err := printer.Run(api.CascadeIgnore, "", nil)
return err
})
for _, c := range containers {
printer.HandleEvent(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: getContainerNameWithoutProject(c),
ID: c.ID,
Service: c.Labels[api.ServiceLabel],
})
monitor := newMonitor(s.apiClient(), projectName)
if len(options.Services) > 0 {
monitor.withServices(options.Services)
} else if options.Project != nil {
monitor.withServices(options.Project.ServiceNames())
}
eg.Go(func() error {
err := s.watchContainers(ctx, projectName, options.Services, nil, printer.HandleEvent, containers, func(c container.Summary, t time.Time) error {
printer.HandleEvent(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: getContainerNameWithoutProject(c),
ID: c.ID,
Service: c.Labels[api.ServiceLabel],
})
monitor.withListener(printer.HandleEvent)
monitor.withListener(func(event api.ContainerEvent) {
if event.Type == api.ContainerEventStarted {
eg.Go(func() error {
err := s.logContainers(ctx, consumer, c, api.LogOptions{
ctr, err := s.apiClient().ContainerInspect(ctx, event.ID)
if err != nil {
return err
}
err = s.doLogContainer(ctx, consumer, event.Source, ctr, api.LogOptions{
Follow: options.Follow,
Since: t.Format(time.RFC3339Nano),
Since: ctr.State.StartedAt,
Until: options.Until,
Tail: options.Tail,
Timestamps: options.Timestamps,
})
var notImplErr errdefs.ErrNotImplemented
if errors.As(err, &notImplErr) {
if errdefs.IsNotImplemented(err) {
// ignore
return nil
}
return err
})
return nil
}, func(c container.Summary, t time.Time) error {
printer.HandleEvent(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: "", // actual name will be set by start event
ID: c.ID,
Service: c.Labels[api.ServiceLabel],
})
return nil
})
printer.Stop()
return err
}
})
eg.Go(func() error {
return monitor.Start(ctx)
})
}
return eg.Wait()
}
func (s *composeService) logContainers(ctx context.Context, consumer api.LogConsumer, c container.Summary, options api.LogOptions) error {
cnt, err := s.apiClient().ContainerInspect(ctx, c.ID)
func (s *composeService) logContainer(ctx context.Context, consumer api.LogConsumer, c container.Summary, options api.LogOptions) error {
ctr, err := s.apiClient().ContainerInspect(ctx, c.ID)
if err != nil {
return err
}
name := getContainerNameWithoutProject(c)
return s.doLogContainer(ctx, consumer, name, ctr, options)
}
r, err := s.apiClient().ContainerLogs(ctx, cnt.ID, container.LogsOptions{
func (s *composeService) doLogContainer(ctx context.Context, consumer api.LogConsumer, name string, ctr container.InspectResponse, options api.LogOptions) error {
r, err := s.apiClient().ContainerLogs(ctx, ctr.ID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: options.Follow,
@ -151,11 +135,10 @@ func (s *composeService) logContainers(ctx context.Context, consumer api.LogCons
}
defer r.Close() //nolint:errcheck
name := getContainerNameWithoutProject(c)
w := utils.GetWriter(func(line string) {
consumer.Log(name, line)
})
if cnt.Config.Tty {
if ctr.Config.Tty {
_, err = io.Copy(w, r)
} else {
_, err = stdcopy.StdCopy(w, w, r)

View File

@ -189,8 +189,6 @@ func (l *testLogConsumer) Err(containerName, message string) {
func (l *testLogConsumer) Status(containerName, msg string) {}
func (l *testLogConsumer) Register(containerName string) {}
func (l *testLogConsumer) LogsForContainer(containerName string) []string {
l.mu.Lock()
defer l.mu.Unlock()

254
pkg/compose/model.go Normal file
View File

@ -0,0 +1,254 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os/exec"
"slices"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
func (s *composeService) ensureModels(ctx context.Context, project *types.Project, quietPull bool) error {
if len(project.Models) == 0 {
return nil
}
api, err := s.newModelAPI(project)
if err != nil {
return err
}
availableModels, err := api.ListModels(ctx)
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
return api.SetModelVariables(ctx, project)
})
w := progress.ContextWriter(ctx)
for name, config := range project.Models {
if config.Name == "" {
config.Name = name
}
eg.Go(func() error {
if !slices.Contains(availableModels, config.Model) {
err = api.PullModel(ctx, config, quietPull, w)
if err != nil {
return err
}
}
return api.ConfigureModel(ctx, config, w)
})
}
return eg.Wait()
}
type modelAPI struct {
path string
env []string
prepare func(ctx context.Context, cmd *exec.Cmd) error
}
func (s *composeService) newModelAPI(project *types.Project) (*modelAPI, error) {
dockerModel, err := manager.GetPlugin("model", s.dockerCli, &cobra.Command{})
if err != nil {
if errdefs.IsNotFound(err) {
return nil, fmt.Errorf("'models' support requires Docker Model plugin")
}
return nil, err
}
return &modelAPI{
path: dockerModel.Path,
prepare: func(ctx context.Context, cmd *exec.Cmd) error {
return s.prepareShellOut(ctx, project.Environment, cmd)
},
env: project.Environment.Values(),
}, nil
}
func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, w progress.Writer) error {
w.Event(progress.Event{
ID: model.Name,
Status: progress.Working,
Text: "Pulling",
})
cmd := exec.CommandContext(ctx, m.path, "pull", model.Model)
err := m.prepare(ctx, cmd)
if err != nil {
return err
}
stream, err := cmd.StdoutPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
scanner := bufio.NewScanner(stream)
for scanner.Scan() {
msg := scanner.Text()
if msg == "" {
continue
}
if !quietPull {
w.Event(progress.Event{
ID: model.Name,
Status: progress.Working,
Text: "Pulling",
StatusText: msg,
})
}
}
err = cmd.Wait()
if err != nil {
w.Event(progress.ErrorMessageEvent(model.Name, err.Error()))
}
w.Event(progress.Event{
ID: model.Name,
Status: progress.Working,
Text: "Pulled",
})
return err
}
func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, w progress.Writer) error {
w.Event(progress.Event{
ID: config.Name,
Status: progress.Working,
Text: "Configuring",
})
// configure [--context-size=<n>] MODEL [-- <runtime-flags...>]
args := []string{"configure"}
if config.ContextSize > 0 {
args = append(args, "--context-size", strconv.Itoa(config.ContextSize))
}
args = append(args, config.Model)
if len(config.RuntimeFlags) != 0 {
args = append(args, "--")
args = append(args, config.RuntimeFlags...)
}
cmd := exec.CommandContext(ctx, m.path, args...)
err := m.prepare(ctx, cmd)
if err != nil {
return err
}
return cmd.Run()
}
func (m *modelAPI) SetModelVariables(ctx context.Context, project *types.Project) error {
cmd := exec.CommandContext(ctx, m.path, "status", "--json")
err := m.prepare(ctx, cmd)
if err != nil {
return err
}
statusOut, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error checking docker-model status: %w", err)
}
type Status struct {
Endpoint string `json:"endpoint"`
}
var status Status
err = json.Unmarshal(statusOut, &status)
if err != nil {
return err
}
for _, service := range project.Services {
for ref, modelConfig := range service.Models {
model := project.Models[ref]
varPrefix := strings.ReplaceAll(strings.ToUpper(ref), "-", "_")
var variable string
if modelConfig != nil && modelConfig.ModelVariable != "" {
variable = modelConfig.ModelVariable
} else {
variable = varPrefix + "_MODEL"
}
service.Environment[variable] = &model.Model
if modelConfig != nil && modelConfig.EndpointVariable != "" {
variable = modelConfig.EndpointVariable
} else {
variable = varPrefix + "_URL"
}
service.Environment[variable] = &status.Endpoint
}
}
return nil
}
type Model struct {
Id string `json:"id"`
Tags []string `json:"tags"`
Created int `json:"created"`
Config struct {
Format string `json:"format"`
Quantization string `json:"quantization"`
Parameters string `json:"parameters"`
Architecture string `json:"architecture"`
Size string `json:"size"`
} `json:"config"`
}
func (m *modelAPI) ListModels(ctx context.Context) ([]string, error) {
cmd := exec.CommandContext(ctx, m.path, "ls", "--json")
err := m.prepare(ctx, cmd)
if err != nil {
return nil, err
}
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("error checking available models: %w", err)
}
type AvailableModel struct {
Id string `json:"id"`
Tags []string `json:"tags"`
Created int `json:"created"`
}
models := []AvailableModel{}
err = json.Unmarshal(output, &models)
if err != nil {
return nil, fmt.Errorf("error unmarshalling available models: %w", err)
}
var availableModels []string
for _, model := range models {
availableModels = append(availableModels, model.Tags...)
}
return availableModels, nil
}

218
pkg/compose/monitor.go Normal file
View File

@ -0,0 +1,218 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"strconv"
"github.com/containerd/errdefs"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
)
type monitor struct {
api client.APIClient
project string
// services tells us which service to consider and those we can ignore, maybe ran by a concurrent compose command
services map[string]bool
listeners []api.ContainerEventListener
}
func newMonitor(api client.APIClient, project string) *monitor {
return &monitor{
api: api,
project: project,
services: map[string]bool{},
}
}
func (c *monitor) withServices(services []string) {
for _, name := range services {
c.services[name] = true
}
}
// Start runs monitor to detect application events and return after termination
//
//nolint:gocyclo
func (c *monitor) Start(ctx context.Context) error {
// collect initial application container
initialState, err := c.api.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters.NewArgs(
projectFilter(c.project),
oneOffFilter(false),
hasConfigHashLabel(),
),
})
if err != nil {
return err
}
// containers is the set if container IDs the application is based on
containers := utils.Set[string]{}
for _, ctr := range initialState {
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
containers.Add(ctr.ID)
}
}
restarting := utils.Set[string]{}
evtCh, errCh := c.api.Events(context.Background(), events.ListOptions{
Filters: filters.NewArgs(
filters.Arg("type", "container"),
projectFilter(c.project)),
})
for {
if len(containers) == 0 {
return nil
}
select {
case err := <-errCh:
return err
case event := <-evtCh:
if len(c.services) > 0 && !c.services[event.Actor.Attributes[api.ServiceLabel]] {
continue
}
ctr, err := c.getContainerSummary(event)
if err != nil {
return err
}
switch event.Action {
case events.ActionCreate:
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
containers.Add(ctr.ID)
}
evtType := api.ContainerEventCreated
if _, ok := ctr.Labels[api.ContainerReplaceLabel]; ok {
evtType = api.ContainerEventRecreated
}
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, evtType))
}
logrus.Debugf("container %s created", ctr.Name)
case events.ActionStart:
restarted := restarting.Has(ctr.ID)
if restarted {
logrus.Debugf("container %s restarted", ctr.Name)
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventStarted, func(e *api.ContainerEvent) {
e.Restarting = restarted
}))
}
} else {
logrus.Debugf("container %s started", ctr.Name)
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventStarted))
}
}
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
containers.Add(ctr.ID)
}
case events.ActionRestart:
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventRestarted))
}
logrus.Debugf("container %s restarted", ctr.Name)
case events.ActionStop:
// when a container is in restarting phase, and we stop the application (abort-on-container-exit)
// we won't get any additional start+die events, just this stop as a proof container is down
logrus.Debugf("container %s stopped", ctr.Name)
containers.Remove(ctr.ID)
case events.ActionDie:
logrus.Debugf("container %s exited with code %d", ctr.Name, ctr.ExitCode)
inspect, err := c.api.ContainerInspect(ctx, event.Actor.ID)
if errdefs.IsNotFound(err) {
// Source is already removed
} else if err != nil {
return err
}
if inspect.State != nil && inspect.State.Restarting || inspect.State.Running {
// State.Restarting is set by engine when container is configured to restart on exit
// on ContainerRestart it doesn't (see https://github.com/moby/moby/issues/45538)
// container state still is reported as "running"
logrus.Debugf("container %s is restarting", ctr.Name)
restarting.Add(ctr.ID)
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventExited, func(e *api.ContainerEvent) {
e.Restarting = true
}))
}
} else {
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventExited))
}
containers.Remove(ctr.ID)
}
}
}
}
}
func newContainerEvent(timeNano int64, ctr *api.ContainerSummary, eventType int, opts ...func(e *api.ContainerEvent)) api.ContainerEvent {
name := ctr.Name
defaultName := getDefaultContainerName(ctr.Project, ctr.Labels[api.ServiceLabel], ctr.Labels[api.ContainerNumberLabel])
if name == defaultName {
// remove project- prefix
name = name[len(ctr.Project)+1:]
}
event := api.ContainerEvent{
Type: eventType,
Container: ctr,
Time: timeNano,
Source: name,
ID: ctr.ID,
Service: ctr.Service,
ExitCode: ctr.ExitCode,
}
for _, opt := range opts {
opt(&event)
}
return event
}
func (c *monitor) getContainerSummary(event events.Message) (*api.ContainerSummary, error) {
ctr := &api.ContainerSummary{
ID: event.Actor.ID,
Name: event.Actor.Attributes["name"],
Project: c.project,
Service: event.Actor.Attributes[api.ServiceLabel],
Labels: event.Actor.Attributes, // More than just labels, but that'c the closest the API gives us
}
if ec, ok := event.Actor.Attributes["exitCode"]; ok {
exitCode, err := strconv.Atoi(ec)
if err != nil {
return nil, err
}
ctr.ExitCode = exitCode
}
return ctr, nil
}
func (c *monitor) withListener(listener api.ContainerEventListener) {
c.listeners = append(c.listeners, listener)
}

View File

@ -27,16 +27,15 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/config"
"github.com/docker/compose/v2/pkg/progress"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
type JsonMessage struct {
@ -52,6 +51,8 @@ const (
providerMetadataDirectory = "compose/providers"
)
var mux sync.Mutex
func (s *composeService) runPlugin(ctx context.Context, project *types.Project, service types.ServiceConfig, command string) error {
provider := *service.Provider
@ -70,6 +71,8 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
return err
}
mux.Lock()
defer mux.Unlock()
for name, s := range project.Services {
if _, ok := s.DependsOn[service.Name]; ok {
prefix := strings.ToUpper(service.Name) + "_"
@ -161,14 +164,14 @@ func (s *composeService) getPluginBinaryPath(provider string) (path string, err
if err == nil {
path = plugin.Path
}
if manager.IsNotFound(err) {
if errdefs.IsNotFound(err) {
path, err = exec.LookPath(executable(provider))
}
return path, err
}
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) (*exec.Cmd, error) {
cmdOptionsMetadata := s.getPluginMetadata(path, service.Provider.Type)
cmdOptionsMetadata := s.getPluginMetadata(path, service.Provider.Type, project)
var currentCommandMetadata CommandMetadata
switch command {
case "up":
@ -176,8 +179,9 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
case "down":
currentCommandMetadata = cmdOptionsMetadata.Down
}
commandMetadataIsEmpty := len(currentCommandMetadata.Parameters) == 0
provider := *service.Provider
commandMetadataIsEmpty := cmdOptionsMetadata.IsEmpty()
if err := currentCommandMetadata.CheckRequiredParameters(provider); !commandMetadataIsEmpty && err != nil {
return nil, err
}
@ -193,34 +197,21 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
args = append(args, service.Name)
cmd := exec.CommandContext(ctx, path, args...)
// exec provider command with same environment Compose is running
env := types.NewMapping(os.Environ())
// but remove DOCKER_CLI_PLUGIN... variable so plugin can detect it run standalone
delete(env, manager.ReexecEnvvar)
// and add the explicit environment variables set for service
for key, val := range service.Environment.RemoveEmpty().ToMapping() {
env[key] = val
err := s.prepareShellOut(ctx, project.Environment, cmd)
if err != nil {
return nil, err
}
cmd.Env = env.Values()
// Use docker/cli mechanism to propagate termination signal to child process
server, err := socket.NewPluginServer(nil)
if err == nil {
defer server.Close() //nolint:errcheck
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
}
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, &carrier)
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
return cmd, nil
}
func (s *composeService) getPluginMetadata(path, command string) ProviderMetadata {
func (s *composeService) getPluginMetadata(path, command string, project *types.Project) ProviderMetadata {
cmd := exec.Command(path, "compose", "metadata")
err := s.prepareShellOut(context.Background(), project.Environment, cmd)
if err != nil {
logrus.Debugf("failed to prepare plugin metadata command: %v", err)
return ProviderMetadata{}
}
stdout := &bytes.Buffer{}
cmd.Stdout = stdout
@ -255,6 +246,10 @@ type ProviderMetadata struct {
Down CommandMetadata `json:"down"`
}
func (p ProviderMetadata) IsEmpty() bool {
return p.Description == "" && p.Up.Parameters == nil && p.Down.Parameters == nil
}
type CommandMetadata struct {
Parameters []ParameterMetadata `json:"parameters"`
}

View File

@ -18,7 +18,6 @@ package compose
import (
"fmt"
"sync"
"github.com/docker/compose/v2/pkg/api"
)
@ -26,137 +25,29 @@ import (
// logPrinter watch application containers and collect their logs
type logPrinter interface {
HandleEvent(event api.ContainerEvent)
Run(cascade api.Cascade, exitCodeFrom string, stopFn func() error) (int, error)
Cancel()
Stop()
}
type printer struct {
queue chan api.ContainerEvent
consumer api.LogConsumer
stopCh chan struct{} // stopCh is a signal channel for producers to stop sending events to the queue
stop sync.Once
}
// newLogPrinter builds a LogPrinter passing containers logs to LogConsumer
func newLogPrinter(consumer api.LogConsumer) logPrinter {
printer := printer{
consumer: consumer,
queue: make(chan api.ContainerEvent),
stopCh: make(chan struct{}),
stop: sync.Once{},
}
return &printer
}
func (p *printer) Cancel() {
// note: HandleEvent is used to ensure this doesn't deadlock
p.HandleEvent(api.ContainerEvent{Type: api.UserCancel})
}
func (p *printer) Stop() {
p.stop.Do(func() {
close(p.stopCh)
for {
select {
case <-p.queue:
// purge the queue to free producers goroutines
// p.queue will be garbage collected
default:
return
}
}
})
}
func (p *printer) HandleEvent(event api.ContainerEvent) {
select {
case <-p.stopCh:
return
default:
p.queue <- event
}
}
//nolint:gocyclo
func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() error) (int, error) {
var (
aborting bool
exitCode int
)
defer p.Stop()
// containers we are tracking. Use true when container is running, false after we receive a stop|die signal
containers := map[string]bool{}
for {
select {
case <-p.stopCh:
return exitCode, nil
case event := <-p.queue:
container, id := event.Container, event.ID
switch event.Type {
case api.UserCancel:
aborting = true
case api.ContainerEventAttach:
if attached, ok := containers[id]; ok && attached {
continue
}
containers[id] = true
p.consumer.Register(container)
case api.ContainerEventExit, api.ContainerEventStopped, api.ContainerEventRecreated:
if !aborting && containers[id] {
p.consumer.Status(container, fmt.Sprintf("exited with code %d", event.ExitCode))
if event.Type == api.ContainerEventRecreated {
p.consumer.Status(container, "has been recreated")
}
}
containers[id] = false
if !event.Restarting {
delete(containers, id)
}
if cascade == api.CascadeStop {
if !aborting {
aborting = true
err := stopFn()
if err != nil {
return 0, err
}
}
}
if event.Type == api.ContainerEventExit {
if cascade == api.CascadeFail && event.ExitCode != 0 {
exitCodeFrom = event.Service
if !aborting {
aborting = true
err := stopFn()
if err != nil {
return 0, err
}
}
}
if cascade == api.CascadeStop && exitCodeFrom == "" {
exitCodeFrom = event.Service
}
}
if exitCodeFrom == event.Service && (event.Type == api.ContainerEventExit || event.Type == api.ContainerEventStopped) {
// Container was interrupted or exited, let's capture exit code
exitCode = event.ExitCode
}
if len(containers) == 0 {
// Last container terminated, done
return exitCode, nil
}
case api.ContainerEventLog, api.HookEventLog:
if !aborting {
p.consumer.Log(container, event.Line)
}
case api.ContainerEventErr:
if !aborting {
p.consumer.Err(container, event.Line)
}
}
}
switch event.Type {
case api.ContainerEventExited:
p.consumer.Status(event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
case api.ContainerEventRecreated:
p.consumer.Status(event.Container.Labels[api.ContainerReplaceLabel], "has been recreated")
case api.ContainerEventLog, api.HookEventLog:
p.consumer.Log(event.Source, event.Line)
case api.ContainerEventErr:
p.consumer.Err(event.Source, event.Line)
}
}

View File

@ -21,6 +21,7 @@ import (
"sort"
"strings"
"github.com/docker/docker/api/types/container"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api"
@ -42,13 +43,13 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
}
summary := make([]api.ContainerSummary, len(containers))
eg, ctx := errgroup.WithContext(ctx)
for i, container := range containers {
for i, ctr := range containers {
eg.Go(func() error {
publishers := make([]api.PortPublisher, len(container.Ports))
sort.Slice(container.Ports, func(i, j int) bool {
return container.Ports[i].PrivatePort < container.Ports[j].PrivatePort
publishers := make([]api.PortPublisher, len(ctr.Ports))
sort.Slice(ctr.Ports, func(i, j int) bool {
return ctr.Ports[i].PrivatePort < ctr.Ports[j].PrivatePort
})
for i, p := range container.Ports {
for i, p := range ctr.Ports {
publishers[i] = api.PortPublisher{
URL: p.IP,
TargetPort: int(p.PrivatePort),
@ -57,22 +58,22 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
}
}
inspect, err := s.apiClient().ContainerInspect(ctx, container.ID)
inspect, err := s.apiClient().ContainerInspect(ctx, ctr.ID)
if err != nil {
return err
}
var (
health string
health container.HealthStatus
exitCode int
)
if inspect.State != nil {
switch inspect.State.Status {
case "running":
case container.StateRunning:
if inspect.State.Health != nil {
health = inspect.State.Health.Status
}
case "exited", "dead":
case container.StateExited, container.StateDead:
exitCode = inspect.State.ExitCode
}
}
@ -81,7 +82,7 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
local int
mounts []string
)
for _, m := range container.Mounts {
for _, m := range ctr.Mounts {
name := m.Name
if name == "" {
name = m.Source
@ -93,26 +94,26 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
}
var networks []string
if container.NetworkSettings != nil {
for k := range container.NetworkSettings.Networks {
if ctr.NetworkSettings != nil {
for k := range ctr.NetworkSettings.Networks {
networks = append(networks, k)
}
}
summary[i] = api.ContainerSummary{
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,
ID: ctr.ID,
Name: getCanonicalContainerName(ctr),
Names: ctr.Names,
Image: ctr.Image,
Project: ctr.Labels[api.ProjectLabel],
Service: ctr.Labels[api.ServiceLabel],
Command: ctr.Command,
State: ctr.State,
Status: ctr.Status,
Created: ctr.Created,
Labels: ctr.Labels,
SizeRw: ctr.SizeRw,
SizeRootFs: ctr.SizeRootFs,
Mounts: mounts,
LocalVolumes: local,
Networks: networks,

View File

@ -22,11 +22,11 @@ import (
"testing"
containerType "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
compose "github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/filters"
)
func TestPs(t *testing.T) {
@ -42,10 +42,10 @@ func TestPs(t *testing.T) {
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)), hasConfigHashLabel())
args.Add("label", "com.docker.compose.oneoff=False")
listOpts := containerType.ListOptions{Filters: args, All: false}
c1, inspect1 := containerDetails("service1", "123", "running", "healthy", 0)
c2, inspect2 := containerDetails("service1", "456", "running", "", 0)
c1, inspect1 := containerDetails("service1", "123", containerType.StateRunning, containerType.Healthy, 0)
c2, inspect2 := containerDetails("service1", "456", containerType.StateRunning, "", 0)
c2.Ports = []containerType.Port{{PublicPort: 80, PrivatePort: 90, IP: "localhost"}}
c3, inspect3 := containerDetails("service2", "789", "exited", "", 130)
c3, inspect3 := containerDetails("service2", "789", containerType.StateExited, "", 130)
api.EXPECT().ContainerList(ctx, listOpts).Return([]containerType.Summary{c1, c2, c3}, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "123").Return(inspect1, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "456").Return(inspect2, nil)
@ -56,7 +56,9 @@ func TestPs(t *testing.T) {
expected := []compose.ContainerSummary{
{
ID: "123", Name: "123", Names: []string{"/123"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: "running", Health: "healthy", Publishers: []compose.PortPublisher{},
State: containerType.StateRunning,
Health: containerType.Healthy,
Publishers: []compose.PortPublisher{},
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
@ -66,7 +68,8 @@ func TestPs(t *testing.T) {
},
{
ID: "456", Name: "456", Names: []string{"/456"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
State: "running", Health: "",
State: containerType.StateRunning,
Health: "",
Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90, PublishedPort: 80}},
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
@ -77,7 +80,10 @@ func TestPs(t *testing.T) {
},
{
ID: "789", Name: "789", Names: []string{"/789"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service2",
State: "exited", Health: "", ExitCode: 130, Publishers: []compose.PortPublisher{},
State: containerType.StateExited,
Health: "",
ExitCode: 130,
Publishers: []compose.PortPublisher{},
Labels: map[string]string{
compose.ProjectLabel: strings.ToLower(testProject),
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
@ -90,8 +96,8 @@ func TestPs(t *testing.T) {
assert.DeepEqual(t, containers, expected)
}
func containerDetails(service string, id string, status string, health string, exitCode int) (containerType.Summary, containerType.InspectResponse) {
container := containerType.Summary{
func containerDetails(service string, id string, status containerType.ContainerState, health containerType.HealthStatus, exitCode int) (containerType.Summary, containerType.InspectResponse) {
ctr := containerType.Summary{
ID: id,
Names: []string{"/" + id},
Image: "foo",
@ -107,5 +113,5 @@ func containerDetails(service string, id string, status string, health string, e
},
},
}
return container, inspect
return ctr, inspect
}

View File

@ -236,12 +236,24 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp
if ok, err := s.checkOnlyBuildSection(project); !ok || err != nil {
return false, err
}
if ok, err := s.checkForBindMount(project); !ok || err != nil {
return false, err
}
if options.AssumeYes {
return true, nil
}
bindMounts := s.checkForBindMount(project)
if len(bindMounts) > 0 {
fmt.Println("you are about to publish bind mounts declaration within your OCI artifact.\n" +
"only the bind mount declarations will be added to the OCI artifact (not content)\n" +
"please double check that you are not mounting potential user's sensitive directories or data")
for key, val := range bindMounts {
_, _ = fmt.Fprintln(s.dockerCli.Out(), key)
for _, v := range val {
_, _ = fmt.Fprintf(s.dockerCli.Out(), "%s\n", v.String())
}
}
if ok, err := acceptPublishBindMountDeclarations(s.dockerCli); err != nil || !ok {
return false, err
}
}
detectedSecrets, err := s.checkForSensitiveData(project)
if err != nil {
return false, err
@ -325,6 +337,12 @@ func acceptPublishSensitiveData(cli command.Cli) (bool, error) {
return confirm, err
}
func acceptPublishBindMountDeclarations(cli command.Cli) (bool, error) {
msg := "Are you ok to publish these bind mount declarations? [y/N]: "
confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false)
return confirm, err
}
func envFileLayers(project *types.Project) []ocipush.Pushable {
var layers []ocipush.Pushable
for _, service := range project.Services {
@ -361,15 +379,20 @@ func (s *composeService) checkOnlyBuildSection(project *types.Project) (bool, er
return true, nil
}
func (s *composeService) checkForBindMount(project *types.Project) (bool, error) {
for name, config := range project.Services {
func (s *composeService) checkForBindMount(project *types.Project) map[string][]types.ServiceVolumeConfig {
allFindings := map[string][]types.ServiceVolumeConfig{}
for serviceName, config := range project.Services {
bindMounts := []types.ServiceVolumeConfig{}
for _, volume := range config.Volumes {
if volume.Type == types.VolumeTypeBind {
return false, fmt.Errorf("cannot publish compose file: service %q relies on bind-mount. You should use volumes", name)
bindMounts = append(bindMounts, volume)
}
}
if len(bindMounts) > 0 {
allFindings[serviceName] = bindMounts
}
}
return true, nil
return allFindings
}
func (s *composeService) checkForSensitiveData(project *types.Project) ([]secrets.DetectedSecret, error) {

View File

@ -34,11 +34,10 @@ import (
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"github.com/hashicorp/go-multierror"
"github.com/opencontainers/go-digest"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/internal/registry"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
)
@ -152,7 +151,7 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
if opts.IgnoreFailures {
return nil
}
return multierror.Append(nil, pullErrors...).ErrorOrNil()
return errors.Join(pullErrors...)
}
func imageAlreadyPresent(serviceImage string, localImages map[string]api.ImageSummary) bool {
@ -281,13 +280,7 @@ func ImageDigestResolver(ctx context.Context, file *configfile.ConfigFile, apiCl
}
func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return "", err
}
key := registry.GetAuthConfigKey(repoInfo.Index)
authConfig, err := configFile.GetAuthConfig(key)
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(ref))
if err != nil {
return "", err
}

View File

@ -29,11 +29,10 @@ import (
"github.com/distribution/reference"
"github.com/docker/buildx/driver"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/internal/registry"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
)
@ -51,14 +50,6 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(s.maxConcurrency)
info, err := s.apiClient().Info(ctx)
if err != nil {
return err
}
if info.IndexServerAddress == "" {
info.IndexServerAddress = registry.IndexServer
}
w := progress.ContextWriter(ctx)
for _, service := range project.Services {
if service.Build == nil || service.Image == "" {
@ -79,7 +70,7 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
for _, tag := range tags {
eg.Go(func() error {
err := s.pushServiceImage(ctx, tag, info, s.configFile(), w, options.Quiet)
err := s.pushServiceImage(ctx, tag, s.configFile(), w, options.Quiet)
if err != nil {
if !options.IgnoreFailures {
return err
@ -93,22 +84,13 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
return eg.Wait()
}
func (s *composeService) pushServiceImage(ctx context.Context, tag string, info system.Info, configFile driver.Auth, w progress.Writer, quietPush bool) error {
func (s *composeService) pushServiceImage(ctx context.Context, tag string, configFile driver.Auth, w progress.Writer, quietPush bool) error {
ref, err := reference.ParseNormalizedNamed(tag)
if err != nil {
return err
}
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return err
}
key := repoInfo.Index.Name
if repoInfo.Index.Official {
key = info.IndexServerAddress
}
authConfig, err := configFile.GetAuthConfig(key)
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(ref))
if err != nil {
return err
}

View File

@ -123,11 +123,23 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
return "", err
}
err = s.ensureModels(ctx, project, opts.QuietPull)
if err != nil {
return "", err
}
created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
if err != nil {
return "", err
}
return created.ID, nil
err = s.injectSecrets(ctx, project, service, created.ID)
if err != nil {
return created.ID, err
}
err = s.injectConfigs(ctx, project, service, created.ID)
return created.ID, err
}
func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
@ -176,18 +188,9 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
}
func (s *composeService) startDependencies(ctx context.Context, project *types.Project, options api.RunOptions) error {
var dependencies []string
for name := range project.Services {
if name != options.Service {
dependencies = append(dependencies, name)
}
}
project = project.WithServicesDisabled(options.Service)
project, err := project.WithSelectedServices(dependencies)
if err != nil {
return err
}
err = s.Create(ctx, project, api.CreateOptions{
err := s.Create(ctx, project, api.CreateOptions{
Build: options.Build,
IgnoreOrphans: options.IgnoreOrphans,
RemoveOrphans: options.RemoveOrphans,
@ -197,7 +200,7 @@ func (s *composeService) startDependencies(ctx context.Context, project *types.P
return err
}
if len(dependencies) > 0 {
if len(project.Services) > 0 {
return s.Start(ctx, project.Name, api.StartOptions{
Project: project,
})

61
pkg/compose/shellout.go Normal file
View File

@ -0,0 +1,61 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"os/exec"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/compose/v2/internal"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
// prepareShellOut prepare a shell-out command to be ran by Compose
func (s *composeService) prepareShellOut(gctx context.Context, env types.Mapping, cmd *exec.Cmd) error {
env = env.Clone()
// remove DOCKER_CLI_PLUGIN... variable so a docker-cli plugin will detect it run standalone
delete(env, metadata.ReexecEnvvar)
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(gctx, &carrier)
env.Merge(types.Mapping(carrier))
env["DOCKER_CONTEXT"] = s.dockerCli.CurrentContext()
env["USER_AGENT"] = "compose/" + internal.Version
md, err := s.dockerCli.ContextStore().GetMetadata(s.dockerCli.CurrentContext())
if err != nil {
return err
}
endpoint, err := docker.EndpointFromContext(md)
if err != nil {
return err
}
actualHost := s.dockerCli.DockerEndpoint().Host
if endpoint.Host != actualHost {
// We are running with `--host` or `DOCKER_HOST` which overrides selected context
env["DOCKER_HOST"] = actualHost
}
cmd.Env = env.Values()
return nil
}

View File

@ -20,19 +20,14 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"time"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
containerType "github.com/docker/docker/api/types/container"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/docker/api/types/filters"
"golang.org/x/sync/errgroup"
)
func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
@ -56,60 +51,6 @@ func (s *composeService) start(ctx context.Context, projectName string, options
}
}
// 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 {
_, err := s.attach(attachCtx, project, listener, options.AttachTo)
if err != nil {
return err
}
eg.Go(func() error {
// 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(ctr containerType.Summary, _ time.Time) error {
svc := ctr.Labels[api.ServiceLabel]
if attachTo.Has(svc) {
return s.attachContainer(attachCtx, ctr, listener)
}
// HACK: simulate an "attach" event
listener(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: getContainerNameWithoutProject(ctr),
ID: ctr.ID,
Service: svc,
})
return nil
}, func(ctr containerType.Summary, _ time.Time) error {
listener(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: "", // actual name will be set by start event
ID: ctr.ID,
Service: ctr.Labels[api.ServiceLabel],
})
return nil
})
})
}
var containers Containers
containers, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
Filters: filters.NewArgs(
@ -157,7 +98,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
}
}
return eg.Wait()
return nil
}
// getDependencyCondition checks if service is depended on by other services
@ -173,182 +114,3 @@ func getDependencyCondition(service types.ServiceConfig, project *types.Project)
}
return ServiceConditionRunningOrHealthy
}
type containerWatchFn func(ctr containerType.Summary, t time.Time) error
// watchContainers uses engine events to capture container start/die and notify ContainerEventListener
func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
projectName string, services, required []string,
listener api.ContainerEventListener, containers Containers, onStart, onRecreate containerWatchFn,
) error {
if len(containers) == 0 {
return nil
}
if len(required) == 0 {
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 containerType.Summary) bool {
if len(services) > 0 {
// we only watch some services
return slices.Contains(services, c.Labels[api.ServiceLabel])
}
return true
}
// predicate to tell if a container we receive event for should be watched until termination
isRequired := func(c containerType.Summary) bool {
if len(services) > 0 && len(required) > 0 {
// we only watch some services
return slices.Contains(required, c.Labels[api.ServiceLabel])
}
return true
}
var (
expected = utils.NewSet[string]()
watched = map[string]int{}
replaced []string
)
for _, c := range containers {
if isRequired(c) {
expected.Add(c.ID)
}
watched[c.ID] = 0
}
ctx, stop := context.WithCancel(ctx)
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 cerrdefs.IsNotFound(err) {
// it's possible to get "destroy" or "kill" events but not
// be able to inspect in time before they're gone from the
// API, so just remove the watch without erroring
delete(watched, event.Container)
expected.Remove(event.Container)
return nil
}
return err
}
container := containerType.Summary{
ID: inspected.ID,
Names: []string{inspected.Name},
Labels: inspected.Config.Labels,
}
name := getContainerNameWithoutProject(container)
service := container.Labels[api.ServiceLabel]
switch event.Status {
case "stop":
if inspected.State.Running {
// on sync+restart action the container stops -> dies -> start -> restart
// we do not want to stop the current container, we want to restart it
return nil
}
if _, ok := watched[container.ID]; ok {
eType := api.ContainerEventStopped
if slices.Contains(replaced, container.ID) {
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
eType = api.ContainerEventRecreated
}
listener(api.ContainerEvent{
Type: eType,
Container: name,
ID: container.ID,
Service: service,
ExitCode: inspected.State.ExitCode,
})
}
delete(watched, container.ID)
expected.Remove(container.ID)
case "die":
restarted := watched[container.ID]
watched[container.ID] = restarted + 1
// Container terminated.
willRestart := inspected.State.Restarting
if inspected.State.Running {
// on sync+restart action inspected.State.Restarting is false,
// however the container is already running before it restarts
willRestart = true
}
eType := api.ContainerEventExit
if slices.Contains(replaced, container.ID) {
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
eType = api.ContainerEventRecreated
}
listener(api.ContainerEvent{
Type: eType,
Container: name,
ID: container.ID,
Service: service,
ExitCode: inspected.State.ExitCode,
Restarting: willRestart,
})
if !willRestart {
// we're done with this one
delete(watched, container.ID)
expected.Remove(container.ID)
}
case "start":
count, ok := watched[container.ID]
mustAttach := ok && count > 0 // Container restarted, need to re-attach
if !ok {
// A new container has just been added to service by scale
watched[container.ID] = 0
expected.Add(container.ID)
mustAttach = true
}
if mustAttach {
// Container restarted, need to re-attach
err := onStart(container, event.Timestamp)
if err != nil {
return err
}
}
case "create":
if id, ok := container.Labels[api.ContainerReplaceLabel]; ok {
replaced = append(replaced, id)
err = onRecreate(container, event.Timestamp)
if err != nil {
return err
}
if expected.Has(id) {
expected.Add(inspected.ID)
expected.Add(container.ID)
}
watched[container.ID] = 1
} else if ofInterest(container) {
watched[container.ID] = 1
if isRequired(container) {
expected.Add(container.ID)
}
}
}
return nil
},
})
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return err
}

View File

@ -27,11 +27,11 @@ import (
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
return s.stop(ctx, strings.ToLower(projectName), options)
return s.stop(ctx, strings.ToLower(projectName), options, nil)
}, s.stdinfo(), "Stopping")
}
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions) error {
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions, event api.ContainerEventListener) error {
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
if err != nil {
return err
@ -55,6 +55,6 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a
return nil
}
serv := project.Services[service]
return s.stopContainers(ctx, w, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout)
return s.stopContainers(ctx, w, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout, event)
})
}

View File

@ -18,22 +18,25 @@ package compose
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"slices"
"sync"
"sync/atomic"
"syscall"
"github.com/compose-spec/compose-go/v2/types"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/eiannone/keyboard"
"github.com/hashicorp/go-multierror"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
)
func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo
@ -59,26 +62,20 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
return err
}
var eg multierror.Group
// if we get a second signal during shutdown, we kill the services
// immediately, so the channel needs to have sufficient capacity or
// we might miss a signal while setting up the second channel read
// (this is also why signal.Notify is used vs signal.NotifyContext)
signalChan := make(chan os.Signal, 2)
defer close(signalChan)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(signalChan)
var isTerminated atomic.Bool
printer := newLogPrinter(options.Start.Attach)
watcher, err := NewWatcher(project, options, s.watch)
if err != nil && options.Start.Watch {
return err
}
var navigationMenu *formatter.LogKeyboard
var kEvents <-chan keyboard.KeyEvent
var (
logConsumer = options.Start.Attach
navigationMenu *formatter.LogKeyboard
kEvents <-chan keyboard.KeyEvent
)
if options.Start.NavigationMenu {
kEvents, err = keyboard.GetKeys(100)
if err != nil {
@ -87,31 +84,62 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
} else {
defer keyboard.Close() //nolint:errcheck
isDockerDesktopActive := s.isDesktopIntegrationActive()
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, watcher != nil)
navigationMenu = formatter.NewKeyboardManager(isDockerDesktopActive, signalChan, options.Start.Watch, watcher)
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive)
navigationMenu = formatter.NewKeyboardManager(isDockerDesktopActive, signalChan)
logConsumer = navigationMenu.Decorate(logConsumer)
}
}
watcher, err := NewWatcher(project, options, s.watch, logConsumer)
if err != nil && options.Start.Watch {
return err
}
if navigationMenu != nil && watcher != nil {
navigationMenu.EnableWatch(options.Start.Watch, watcher)
}
printer := newLogPrinter(logConsumer)
// global context to handle canceling goroutines
globalCtx, cancel := context.WithCancel(ctx)
defer cancel()
var (
eg errgroup.Group
mu sync.Mutex
errs []error
)
appendErr := func(err error) {
if err != nil {
mu.Lock()
errs = append(errs, err)
mu.Unlock()
}
}
doneCh := make(chan bool)
eg.Go(func() error {
first := true
gracefulTeardown := func() {
printer.Cancel()
_, _ = fmt.Fprintln(s.stdinfo(), "Gracefully stopping... (press Ctrl+C again to force)")
eg.Go(func() error {
err := s.Stop(context.WithoutCancel(ctx), project.Name, api.StopOptions{
Services: options.Create.Services,
Project: project,
})
isTerminated.Store(true)
return err
})
first = false
fmt.Println("Gracefully Stopping... press Ctrl+C again to force")
eg.Go(func() error {
err := progress.RunWithLog(context.WithoutCancel(globalCtx), func(c context.Context) error {
return s.stop(c, project.Name, api.StopOptions{
Services: options.Create.Services,
Project: project,
}, printer.HandleEvent)
}, s.stdinfo(), logConsumer)
appendErr(err)
return nil
})
isTerminated.Store(true)
}
for {
select {
case <-doneCh:
case <-globalCtx.Done():
if watcher != nil {
return watcher.Stop()
}
@ -122,64 +150,145 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
}
case <-signalChan:
if first {
keyboard.Close() //nolint:errcheck
_ = keyboard.Close()
gracefulTeardown()
break
}
eg.Go(func() error {
err := s.kill(context.WithoutCancel(ctx), project.Name, api.KillOptions{
err := s.kill(context.WithoutCancel(globalCtx), project.Name, api.KillOptions{
Services: options.Create.Services,
Project: project,
All: true,
})
// Ignore errors indicating that some of the containers were already stopped or removed.
if cerrdefs.IsNotFound(err) || cerrdefs.IsConflict(err) {
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) {
return nil
}
return err
appendErr(err)
return nil
})
return nil
case event := <-kEvents:
navigationMenu.HandleKeyEvents(ctx, event, project, options)
navigationMenu.HandleKeyEvents(globalCtx, event, project, options)
}
}
})
var exitCode int
eg.Go(func() error {
code, err := printer.Run(options.Start.OnExit, options.Start.ExitCodeFrom, func() error {
_, _ = fmt.Fprintln(s.stdinfo(), "Aborting on container exit...")
return progress.Run(ctx, func(ctx context.Context) error {
return s.Stop(ctx, project.Name, api.StopOptions{
Services: options.Create.Services,
Project: project,
})
}, s.stdinfo())
})
exitCode = code
return err
})
if options.Start.Watch && watcher != nil {
err = watcher.Start(ctx)
if err != nil {
if err := watcher.Start(globalCtx); err != nil {
// cancel the global context to terminate background goroutines
cancel()
_ = eg.Wait()
return err
}
}
monitor := newMonitor(s.apiClient(), project.Name)
if len(options.Start.Services) > 0 {
monitor.withServices(options.Start.Services)
} else {
// Start.AttachTo have been already curated with only the services to monitor
monitor.withServices(options.Start.AttachTo)
}
monitor.withListener(printer.HandleEvent)
var exitCode int
if options.Start.OnExit != api.CascadeIgnore {
once := true
// detect first container to exit to trigger application shutdown
monitor.withListener(func(event api.ContainerEvent) {
if once && event.Type == api.ContainerEventExited {
if options.Start.OnExit == api.CascadeFail && event.ExitCode == 0 {
return
}
once = false
exitCode = event.ExitCode
_, _ = fmt.Fprintln(s.stdinfo(), progress.ErrorColor("Aborting on container exit..."))
eg.Go(func() error {
err := progress.RunWithLog(context.WithoutCancel(globalCtx), func(c context.Context) error {
return s.stop(c, project.Name, api.StopOptions{
Services: options.Create.Services,
Project: project,
}, printer.HandleEvent)
}, s.stdinfo(), logConsumer)
appendErr(err)
return nil
})
}
})
}
if options.Start.ExitCodeFrom != "" {
once := true
// capture exit code from first container to exit with selected service
monitor.withListener(func(event api.ContainerEvent) {
if once && event.Type == api.ContainerEventExited && event.Service == options.Start.ExitCodeFrom {
exitCode = event.ExitCode
once = false
}
})
}
containers, err := s.attach(globalCtx, project, printer.HandleEvent, options.Start.AttachTo)
if err != nil {
cancel()
_ = eg.Wait()
return err
}
attached := make([]string, len(containers))
for i, ctr := range containers {
attached[i] = ctr.ID
}
monitor.withListener(func(event api.ContainerEvent) {
if event.Type != api.ContainerEventStarted {
return
}
if slices.Contains(attached, event.ID) {
return
}
eg.Go(func() error {
ctr, err := s.apiClient().ContainerInspect(globalCtx, event.ID)
if err != nil {
appendErr(err)
return nil
}
err = s.doLogContainer(globalCtx, options.Start.Attach, event.Source, ctr, api.LogOptions{
Follow: true,
Since: ctr.State.StartedAt,
})
if errdefs.IsNotImplemented(err) {
// container may be configured with logging_driver: none
// as container already started, we might miss the very first logs. But still better than none
err := s.doAttachContainer(globalCtx, event.Service, event.ID, event.Source, printer.HandleEvent)
appendErr(err)
return nil
}
appendErr(err)
return nil
})
})
eg.Go(func() error {
err := monitor.Start(globalCtx)
// cancel the global context to terminate signal-handler goroutines
cancel()
appendErr(err)
return nil
})
// We use the parent context without cancellation as we manage sigterm to stop the stack
err = s.start(context.WithoutCancel(ctx), project.Name, options.Start, printer.HandleEvent)
if err != nil && !isTerminated.Load() { // Ignore error if the process is terminated
cancel()
_ = eg.Wait()
return err
}
// Signal for the signal-handler goroutines to stop
close(doneCh)
printer.Stop()
err = eg.Wait().ErrorOrNil()
_ = eg.Wait()
err = errors.Join(errs...)
if exitCode != 0 {
errMsg := ""
if err != nil {

85
pkg/compose/volumes.go Normal file
View File

@ -0,0 +1,85 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"slices"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
)
func (s *composeService) Volumes(ctx context.Context, project *types.Project, options api.VolumesOptions) ([]api.VolumesSummary, error) {
projectName := project.Name
allContainers, err := s.apiClient().ContainerList(ctx, container.ListOptions{
Filters: filters.NewArgs(projectFilter(projectName)),
})
if err != nil {
return nil, err
}
var containers []container.Summary
if len(options.Services) > 0 {
// filter service containers
for _, c := range allContainers {
if slices.Contains(options.Services, c.Labels[api.ServiceLabel]) {
containers = append(containers, c)
}
}
} else {
containers = allContainers
}
volumesResponse, err := s.apiClient().VolumeList(ctx, volume.ListOptions{
Filters: filters.NewArgs(projectFilter(projectName)),
})
if err != nil {
return nil, err
}
projectVolumes := volumesResponse.Volumes
if len(options.Services) == 0 {
return projectVolumes, nil
}
var volumes []api.VolumesSummary
// create a name lookup of volumes used by containers
serviceVolumes := make(map[string]bool)
for _, container := range containers {
for _, mount := range container.Mounts {
serviceVolumes[mount.Name] = true
}
}
// append if volumes in this project are in serviceVolumes
for _, v := range projectVolumes {
if serviceVolumes[v.Name] {
volumes = append(volumes, v)
}
}
return volumes, nil
}

View File

@ -0,0 +1,89 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
)
func TestVolumes(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockApi, mockCli := prepareMocks(mockCtrl)
tested := composeService{
dockerCli: mockCli,
}
// Create test volumes
vol1 := &volume.Volume{Name: testProject + "_vol1"}
vol2 := &volume.Volume{Name: testProject + "_vol2"}
vol3 := &volume.Volume{Name: testProject + "_vol3"}
// Create test containers with volume mounts
c1 := container.Summary{
Labels: map[string]string{api.ServiceLabel: "service1"},
Mounts: []container.MountPoint{
{Name: testProject + "_vol1"},
{Name: testProject + "_vol2"},
},
}
c2 := container.Summary{
Labels: map[string]string{api.ServiceLabel: "service2"},
Mounts: []container.MountPoint{
{Name: testProject + "_vol3"},
},
}
ctx := context.Background()
project := &types.Project{Name: testProject}
args := filters.NewArgs(projectFilter(testProject))
listOpts := container.ListOptions{Filters: args}
volumeListArgs := filters.NewArgs(projectFilter(testProject))
volumeListOpts := volume.ListOptions{Filters: volumeListArgs}
volumeReturn := volume.ListResponse{
Volumes: []*volume.Volume{vol1, vol2, vol3},
}
containerReturn := []container.Summary{c1, c2}
// Mock API calls
mockApi.EXPECT().ContainerList(ctx, listOpts).Times(2).Return(containerReturn, nil)
mockApi.EXPECT().VolumeList(ctx, volumeListOpts).Times(2).Return(volumeReturn, nil)
// Test without service filter - should return all project volumes
volumeOptions := api.VolumesOptions{}
volumes, err := tested.Volumes(ctx, project, volumeOptions)
expected := []api.VolumesSummary{vol1, vol2, vol3}
assert.NilError(t, err)
assert.DeepEqual(t, volumes, expected)
// Test with service filter - should only return volumes used by service1
volumeOptions = api.VolumesOptions{Services: []string{"service1"}}
volumes, err = tested.Volumes(ctx, project, volumeOptions)
expected = []api.VolumesSummary{vol1, vol2}
assert.NilError(t, err)
assert.DeepEqual(t, volumes, expected)
}

View File

@ -29,14 +29,17 @@ import (
gsync "sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/compose-spec/compose-go/v2/utils"
ccli "github.com/docker/cli/cli/command/container"
pathutil "github.com/docker/compose/v2/internal/paths"
"github.com/docker/compose/v2/internal/sync"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
cutils "github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v2/pkg/watch"
"github.com/compose-spec/compose-go/v2/types"
"github.com/compose-spec/compose-go/v2/utils"
ccli "github.com/docker/cli/cli/command/container"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
@ -55,17 +58,16 @@ type Watcher struct {
errCh chan error
}
func NewWatcher(project *types.Project, options api.UpOptions, w WatchFunc) (*Watcher, error) {
func NewWatcher(project *types.Project, options api.UpOptions, w WatchFunc, consumer api.LogConsumer) (*Watcher, error) {
for i := range project.Services {
service := project.Services[i]
if service.Develop != nil && service.Develop.Watch != nil {
build := options.Create.Build
build.Quiet = true
return &Watcher{
project: project,
options: api.WatchOptions{
LogTo: options.Start.Attach,
LogTo: consumer,
Build: build,
},
watchFn: w,
@ -192,7 +194,6 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti
return nil, err
}
eg, ctx := errgroup.WithContext(ctx)
options.LogTo.Register(api.WatchLogger)
var (
rules []watchRule
@ -234,7 +235,7 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti
var initialSync bool
success, err := trigger.Extensions.Get("x-initialSync", &initialSync)
if err == nil && success && initialSync && isSync(trigger) {
// Need to check initial files are in container that are meant to be synched from watch action
// Need to check initial files are in container that are meant to be synced from watch action
err := s.initialSync(ctx, project, service, trigger, syncer)
if err != nil {
return nil, err
@ -395,7 +396,7 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
return nil, fmt.Errorf("service %s doesn't have a build section, can't apply %s on watch", types.WatchActionRebuild, service.Name)
}
if trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 {
return nil, fmt.Errorf("can't watch with action %q on service %s wihtout a command", types.WatchActionSyncExec, service.Name)
return nil, fmt.Errorf("can't watch with action %q on service %s without a command", types.WatchActionSyncExec, service.Name)
}
config.Watch[i] = trigger
@ -599,6 +600,10 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service(s) %q after changes were detected...", services))
// restrict the build to ONLY this service, not any of its dependencies
options.Build.Services = services
options.Build.Progress = progress.ModePlain
options.Build.Out = cutils.GetWriter(func(line string) {
options.LogTo.Log(api.WatchLogger, line)
})
var (
imageNameToIdMap map[string]string
@ -813,7 +818,7 @@ func (s *composeService) imageCreatedTime(ctx context.Context, project *types.Pr
if err != nil {
return time.Now(), err
}
// Need to get oldest one?
// Need to get the oldest one?
timeCreated, err := time.Parse(time.RFC3339Nano, img.Created)
if err != nil {
return time.Now(), err

View File

@ -71,9 +71,6 @@ func (s stdLogger) Status(containerName, msg string) {
fmt.Printf("%s: %s\n", containerName, msg)
}
func (s stdLogger) Register(containerName string) {
}
func TestWatch_Sync(t *testing.T) {
mockCtrl := gomock.NewController(t)
cli := mocks.NewMockCli(mockCtrl)

View File

@ -580,3 +580,31 @@ func TestBuildSubDependencies(t *testing.T) {
out = res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
}
func TestBuildLongOutputLine(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "build", "long-line")
out := res.Combined()
assert.Check(t, strings.Contains(out, "long-line Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "up", "--build", "long-line")
out = res.Combined()
assert.Check(t, strings.Contains(out, "long-line Built"))
}
func TestBuildDependentImageWithProfile(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "build", "secret-build-test")
out := res.Combined()
assert.Check(t, strings.Contains(out, "secret-build-test Built"))
}

View File

@ -196,4 +196,24 @@ func TestLocalComposeRun(t *testing.T) {
"front", "env")
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
})
t.Run("compose run -rm with stop signal", func(t *testing.T) {
projectName := "run-test"
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "-f", "./fixtures/ps-test/compose.yaml", "run", "--rm", "-d", "nginx")
res.Assert(t, icmd.Success)
res = c.RunDockerCmd(t, "ps", "--quiet", "--filter", "name=run-test-nginx")
containerID := strings.TrimSpace(res.Stdout())
res = c.RunDockerCmd(t, "stop", containerID)
res.Assert(t, icmd.Success)
res = c.RunDockerCmd(t, "ps", "--all", "--filter", "name=run-test-nginx", "--format", "'{{.Names}}'")
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test-nginx"), res.Stdout())
})
t.Run("compose run --env", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--env", "FOO=BAR",
"front", "env")
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
})
}

View File

@ -37,6 +37,11 @@ func TestUnusedMissingEnvFile(t *testing.T) {
defer c.cleanupWithDown(t, "unused_dotenv")
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "up", "-d", "serviceA")
// Runtime operations should work even with missing env file
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "ps")
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "logs")
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "down")
}
func TestRunEnvFile(t *testing.T) {

View File

@ -3,6 +3,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: bridge
name: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge

View File

@ -3,8 +3,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: bridge
namespace: bridge
name: {{ .Values.projectName }}
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
data:

View File

@ -10,13 +10,13 @@ metadata:
com.docker.compose.service: serviceA
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
replicas: {{ .Values.deployment.defaultReplicas }}
selector:
matchLabels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
strategy:
type: Recreate
type: {{ .Values.deployment.strategy }}
template:
metadata:
labels:
@ -28,6 +28,10 @@ spec:
- name: servicea
image: {{ .Values.serviceA.image }}
imagePullPolicy: {{ .Values.serviceA.imagePullPolicy }}
resources:
limits:
cpu: {{ .Values.resources.defaultCpuLimit }}
memory: {{ .Values.resources.defaultMemoryLimit }}
ports:
- name: servicea-8080
containerPort: 8080
@ -39,7 +43,7 @@ spec:
volumes:
- name: etc-my-config1-txt
configMap:
name: bridge
name: {{ .Values.projectName }}
items:
- key: my-config
path: my-config

View File

@ -12,7 +12,7 @@ metadata:
com.docker.compose.service: serviceA
app.kubernetes.io/managed-by: Helm
spec:
type: LoadBalancer
type: {{ .Values.service.type }}
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA

View File

@ -10,13 +10,13 @@ metadata:
com.docker.compose.service: serviceB
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
replicas: {{ .Values.deployment.defaultReplicas }}
selector:
matchLabels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
strategy:
type: Recreate
type: {{ .Values.deployment.strategy }}
template:
metadata:
labels:
@ -29,6 +29,10 @@ spec:
- name: serviceb
image: {{ .Values.serviceB.image }}
imagePullPolicy: {{ .Values.serviceB.imagePullPolicy }}
resources:
limits:
cpu: {{ .Values.resources.defaultCpuLimit }}
memory: {{ .Values.resources.defaultMemoryLimit }}
ports:
- name: serviceb-8082
containerPort: 8082

View File

@ -10,7 +10,7 @@ metadata:
com.docker.compose.service: serviceB
app.kubernetes.io/managed-by: Helm
spec:
type: LoadBalancer
type: {{ .Values.service.type }}
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB

View File

@ -1,8 +1,25 @@
#! values.yaml
# Project Name
projectName: bridge
# Namespace
namespace: bridge
# Default deployment settings
deployment:
strategy: Recreate
defaultReplicas: 1
# Default resource limits
resources:
defaultCpuLimit: "100m"
defaultMemoryLimit: "512Mi"
# Service settings
service:
type: LoadBalancer
# Storage settings
storage:
defaultStorageClass: "hostpath"
defaultSize: "100Mi"
defaultAccessMode: "ReadWriteOnce"
# Services variables
serviceA:
image: alpine
imagePullPolicy: IfNotPresent

View File

@ -0,0 +1,49 @@
# 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.
FROM alpine
# We generate warnings *on purpose* to bloat the JSON output of bake
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT

View File

@ -0,0 +1,5 @@
services:
long-line:
build:
context: .
dockerfile: Dockerfile

View File

@ -0,0 +1,19 @@
# 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.
FROM alpine
RUN --mount=type=secret,id=test-secret ls -la /run/secrets/; cp /run/secrets/test-secret /tmp
CMD ["cat", "/tmp/test-secret"]

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