Compare commits

...

328 Commits

Author SHA1 Message Date
dependabot[bot]
7abaa06617 build(deps): bump go.yaml.in/yaml/v4 from 4.0.0-rc.3 to 4.0.0-rc.4
Bumps [go.yaml.in/yaml/v4](https://github.com/yaml/go-yaml) from 4.0.0-rc.3 to 4.0.0-rc.4.
- [Commits](https://github.com/yaml/go-yaml/compare/v4.0.0-rc.3...v4.0.0-rc.4)

---
updated-dependencies:
- dependency-name: go.yaml.in/yaml/v4
  dependency-version: 4.0.0-rc.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 10:43:48 +01:00
dependabot[bot]
3b0e8f538e build(deps): bump golang.org/x/sys from 0.40.0 to 0.41.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/sys/compare/v0.40.0...v0.41.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 10:31:58 +01:00
Sebastiaan van Stijn
af376603c3 update to go1.25.7
go1.25.7 (released 2026-02-04) includes security fixes to the go command
and the crypto/tls package, as well as bug fixes to the compiler and the
crypto/x509 package. See the Go 1.25.7 milestone on our issue tracker for
details:
https://github.com/golang/go/issues?q=milestone%3AGo1.25.7+label%3ACherryPickApproved

full diff: https://github.com/golang/go/compare/go1.25.6...go1.25.7

From the security mailing list:

> Hello gophers,
>
> We have just released Go versions 1.25.7 and 1.24.13, minor point releases.
>
> These releases include 2 security fixes following the security policy:
>
> - cmd/cgo: remove user-content from doc strings in cgo ASTs
>
>   A discrepancy between how Go and C/C++ comments
>   were parsed allowed for code smuggling into the
>   resulting cgo binary.
>
>   To prevent this behavior, the cgo compiler
>   will no longer parse user-provided doc
>   comments.
>
>   Thank you to RyotaK (https://ryotak.net) of
>   GMO Flatt Security Inc. for reporting this issue.
>
>   This is CVE-2025-61732 and https://go.dev/issue/76697.
>
> - crypto/tls: unexpected session resumption when using Config.GetConfigForClient
>
>   Config.GetConfigForClient is documented to use the original Config's session
>   ticket keys unless explicitly overridden. This can cause unexpected behavior if
>   the returned Config modifies authentication parameters, like ClientCAs: a
>   connection initially established with the parent (or a sibling) Config can be
>   resumed, bypassing the modified authentication requirements.
>
>   If ClientAuth is VerifyClientCertIfGiven or RequireAndVerifyClientCert (on the
>   server) or InsecureSkipVerify is false (on the client), crypto/tls now checks
>   that the root of the previously-verified chain is still in ClientCAs/RootCAs
>   when resuming a connection.
>
>   Go 1.26 Release Candidate 2, Go 1.25.6, and Go 1.24.12 had fixed a similar issue
>   related to session ticket keys being implicitly shared by Config.Clone. Since
>   this fix is broader, the Config.Clone behavior change has been reverted.
>
>   Note that VerifyPeerCertificate still behaves as documented: it does not apply
>   to resumed connections. Applications that use Config.GetConfigForClient or
>   Config.Clone and do not wish to blindly resume connections established with the
>   original Config must use VerifyConnection instead (or SetSessionTicketKeys or
>   SessionTicketsDisabled).
>
>   Thanks to Coia Prant (github.com/rbqvq) for reporting this issue.
>
>   This updates CVE-2025-68121 and Go issue https://go.dev/issue/77217.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-02-09 09:21:32 +01:00
Michael Irwin
7f8814f4c5 Fix invalid path error when using OCI artifacts on Windows
When using OCI artifacts (e.g., `docker compose -f oci://dockersamples/welcome-to-docker up`)
on Windows, users encountered the following error:

  CreateFile C:\Users\username\oci:\dockersamples\.env: The filename, directory name,
  or volume label syntax is incorrect.

This issue was introduced between v5.0.0 and v5.0.1, specifically by commit 6c043929a
which fixed error handling in setEnvWithDotEnv. The bug existed in v5.0.0 but was
silently ignored due to improper error handling.

Root Cause:
-----------
The setEnvWithDotEnv function creates ProjectOptions without registering remote loaders.
Without remote loaders, the compose-go library doesn't recognize OCI paths as remote
resources. It falls through to filepath.Abs() which treats the OCI reference as a
relative path.

On Windows, filepath.Abs("oci://dockersamples/...") produces an invalid path like:
  C:\Users\username\oci:\dockersamples

Windows rejects this path because colons are only valid after drive letters.

Solution:
---------
Modified setEnvWithDotEnv to detect remote config paths and skip environment loading
for them. Instead of hardcoding string checks, the fix uses the actual remote loaders'
Accept() method to determine if a config path is remote. This is more maintainable
and consistent with how the compose-go library identifies remote resources.

The function now:
- Accepts a dockerCli parameter to access remote loaders
- Uses opts.remoteLoaders(dockerCli) to get loader instances
- Checks if any loader accepts the config path using loader.Accept()
- Skips .env loading for remote configs (happens later when loaders are initialized)
- Allows normal processing for local compose files

Testing:
--------
- Added tests for OCI artifacts, Git remotes, and local paths
- Verified fix works on Windows ARM64
- All existing tests pass

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Michael Irwin <mikesir87@gmail.com>
2026-02-09 09:15:37 +01:00
CrazyMax
af0029afe1 ci: use bin-image edge tag directly for e2e tests
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2026-02-03 15:11:49 +01:00
CrazyMax
b76feb66e1 ci: fix missing dependency on bin-image job
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2026-02-03 15:11:49 +01:00
CrazyMax
9dc7f1e70c ci: use docker/github-builder to build, sign and push bin image
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2026-02-03 14:19:21 +01:00
CrazyMax
03205124fe ci: use docker/github-builder to build, sign binaries
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
2026-02-03 14:19:21 +01:00
Sebastiaan van Stijn
8b769bad6b pkg/compose: remove dependency on github.com/docker/buildx/driver
The driver.Auth interface was describing the configfile.GetAuthConfig
implementation; define a local interface instead of using buildx's
definition as an intermediate.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-29 15:59:03 +01:00
Nicolas De Loof
671507a8b3 fix panic
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-29 09:57:49 +01:00
ibrahim yapar
56ab28aef3 compose: recreate container when mounted image digest changes
Until now, mustRecreate logic only checked for divergence in TypeVolume
mounts but ignored TypeImage mounts. This inconsistency caused containers
to erroneously retain stale images even after the source image was rebuilt.
This commit updates ensureImagesExists to resolve image volume sources to
their digests using the official reference package. This enables ServiceHash
(config hash) to naturally detect underlying image digest changes,
triggering recreation via the standard convergence logic.
An E2E test case is added to verify this behavior.
Fixes #13547

Signed-off-by: ibrahim yapar <74625807+ibrahimypr@users.noreply.github.com>
2026-01-26 15:55:44 +01:00
Sebastiaan van Stijn
e7d870a106 update to go1.25.6
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-26 09:26:08 +01:00
Nepomuk Crhonek
d5bb3387ca Fix potential nil pointer dereference in container event monitoring
The condition for checking container restart state had incorrect operator
precedence. The expression:

  inspect.State != nil && inspect.State.Restarting || inspect.State.Running

is evaluated as:

  (inspect.State != nil && inspect.State.Restarting) || inspect.State.Running

This means if inspect.State is nil and inspect.State.Restarting is false
(which would trigger a panic), the code would attempt to access
inspect.State.Running, causing a nil pointer dereference.

This fix adds parentheses to ensure the nil check applies to both
state checks:

  inspect.State != nil && (inspect.State.Restarting || inspect.State.Running)

Signed-off-by: Nepomuk Crhonek <105591323+Nepomuk5665@users.noreply.github.com>
2026-01-25 21:15:37 +01:00
Salman Muin Kayser Chishti
d91fc63813 Upgrade GitHub Actions to latest versions
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
2026-01-23 15:34:48 +01:00
Sebastiaan van Stijn
c51b1fea29 replace some uses of strings.Split(N) for strings.Cut
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-22 11:26:16 +01:00
Sebastiaan van Stijn
fa7549a851 Dockerfile: update golangci-lint to v2.8.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-21 10:11:02 +01:00
Mahesh Thakur
a061c17736 fix: emit container status events after network reconnection
Signed-off-by: Mahesh Thakur <maheshthakur9152@gmail.com>
2026-01-21 09:40:05 +01:00
Sebastiaan van Stijn
c5e7d9158c update to go1.24.12
This releases includes 6 security fixes following the security policy:

- archive/zip: denial of service when parsing arbitrary ZIP archives

    archive/zip used a super-linear file name indexing algorithm that is invoked the first time a file in an archive is opened. This can lead to a denial of service when consuming a maliciously constructed ZIP archive.

    Thanks to Thanks to Jakub Ciolek for reporting this issue.

    This is CVE-2025-61728 and Go issue https://go.dev/issue/77102.

- net/http: memory exhaustion in Request.ParseForm

    When parsing a URL-encoded form net/http may allocate an unexpected amount of
    memory when provided a large number of key-value pairs. This can result in a
    denial of service due to memory exhaustion.

    Thanks to jub0bs for reporting this issue.

    This is CVE-2025-61726 and Go issue https://go.dev/issue/77101.

- crypto/tls: Config.Clone copies automatically generated session ticket keys, session resumption does not account for the expiration of full certificate chain

    The Config.Clone methods allows cloning a Config which has already been passed
    to a TLS function, allowing it to be mutated and reused.

    If Config.SessionTicketKey has not been set, and Config.SetSessionTicketKeys has
    not been called, crypto/tls will generate random session ticket keys and
    automatically rotate them. Config.Clone would copy these automatically generated
    keys into the returned Config, meaning that the two Configs would share session
    ticket keys, allowing sessions created using one Config could be used to resume
    sessions with the other Config. This can allow clients to resume sessions even
    though the Config may be configured such that they should not be able to do so.

    Config.Clone no longer copies the automatically generated session ticket keys.
    Config.Clone still copies keys which are explicitly provided, either by setting
    Config.SessionTicketKey or by calling Config.SetSessionTicketKeys.

    This issue was discoverd by the Go Security team while investigating another
    issue reported by Coia Prant (github.com/rbqvq).

    Additionally, on the server side only the expiration of the leaf certificate, if
    one was provided during the initial handshake, was checked when considering if a
    session could be resumed. This allowed sessions to be resumed if an intermediate
    or root certificate in the chain had expired.

    Session resumption now takes into account of the full chain when determining if
    the session can be resumed.

    Thanks to Coia Prant (github.com/rbqvq) for reporting this issue.

    This is CVE-2025-68121 and Go issue https://go.dev/issue/77113.

- cmd/go: bypass of flag sanitization can lead to arbitrary code execution

    Usage of 'CgoPkgConfig' allowed execution of the pkg-config
    binary with flags that are not explicitly safe-listed.

    To prevent this behavior, compiler flags resulting from usage
    of 'CgoPkgConfig' are sanitized prior to invoking pkg-config.

    Thank you to RyotaK (https://ryotak.net) of GMO Flatt Security Inc.
    for reporting this issue.

    This is CVE-2025-61731 and go.dev/issue/77100.

- cmd/go: unexpected code execution when invoking toolchain

    The Go toolchain supports multiple VCS which are used retrieving modules and
    embedding build information into binaries.

    On systems with Mercurial installed (hg) downloading modules (e.g. via go get or
    go mod download) from non-standard sources (e.g. custom domains) can cause
    unexpected code execution due to how external VCS commands are constructed.

    On systems with Git installed, downloading and building modules with malicious
    version strings could allow an attacker to write to arbitrary files on the
    system the user has access to. This can only be triggered by explicitly
    providing the malicious version strings to the toolchain, and does not affect
    usage of @latest or bare module paths.

    The toolchain now uses safer VCS options to prevent misinterpretation of
    untrusted inputs. In addition, the toolchain now disallows module version
    strings prefixed with a "-" or "/" character.

    Thanks to splitline (@splitline) from DEVCORE Research Team for reporting this
    issue.

    This is CVE-2025-68119 and Go issue https://go.dev/issue/77099.

- crypto/tls: handshake messages may be processed at the incorrect encryption level

    During the TLS 1.3 handshake if multiple messages are sent in records that span
    encryption level boundaries (for instance the Client Hello and Encrypted
    Extensions messages), the subsequent messages may be processed before the
    encryption level changes. This can cause some minor information disclosure if a
    network-local attacker can inject messages during the handshake.

    Thanks to Coia Prant (github.com/rbqvq) for reporting this issue.

    This is CVE-2025-61730 and Go issue https://go.dev/issue/76443

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

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-21 09:27:04 +01:00
Nicolas De Loof
3783b8ada3 fsnotify is set in Dockerfile
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-21 08:47:40 +01:00
Nicolas De Loof
c428a77111 set fsnotify build tag when building for OSX
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-20 13:57:53 +01:00
David Gageot
04b4a832dc chore(lint): add forbidigo rules to enforce t.Context() in tests
Add linter rules to prevent usage of context.Background() and
context.TODO() in test files - t.Context() should be used instead.

The rules only apply to *_test.go files, not production code.

Note: os.Setenv is not covered by forbidigo due to a limitation where
it only catches calls when the return value is assigned. However,
errcheck will flag unchecked os.Setenv calls.

Assisted-By: cagent
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-01-20 11:34:11 +01:00
David Gageot
27faa3b84e test: replace os.MkdirTemp with t.TempDir()
Use t.TempDir() which automatically cleans up the temporary directory
when the test completes, eliminating the need for manual cleanup.

Go 1.14 modernization pattern.

Assisted-By: cagent
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-01-20 11:34:11 +01:00
David Gageot
bcc0401e0e test: replace os.Setenv with t.Setenv()
Use t.Setenv() which automatically restores the original value when
the test completes, eliminating the need for manual cleanup.

Go 1.18 modernization pattern.

Assisted-By: cagent
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-01-20 11:34:11 +01:00
David Gageot
093205121c test: replace context.Background()/context.TODO() with t.Context()
Replace manual context creation with t.Context() which is automatically
cancelled when the test completes.

Go 1.24 modernization pattern.

Assisted-By: cagent
Signed-off-by: David Gageot <david.gageot@docker.com>
2026-01-20 11:34:11 +01:00
Amol Yadav
b92b87dd9c fix: robustly handle large file change batches in watch mode
Ensured all watcher and sync goroutines and channels are robustly closed on context cancellation or error.
Added explicit logging for large batches and context cancellation to prevent stuck processes and ensure graceful shutdown on Ctrl-C.

Signed-off-by: Amol Yadav <amyssnipet@yahoo.com>
2026-01-20 08:34:15 +01:00
hiroto.toyoda
06e1287483 fix: update github.com/moby/term to indirect dependency
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
hiroto.toyoda
d7bdb34ff5 refactor(attach): remove unused stdin from getContainerStream
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
hiroto.toyoda
79d7a8acd6 refactor(attach): simplify attachContainerStreams signature
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
hiroto.toyoda
abd99be4fd refactor(attach): remove unused detach watcher and keep attach behavior
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
hiroto.toyoda
2672d34217 Improve error handling in attach.go
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-19 17:46:55 +01:00
Nicolas De Loof
27bf40357a Bump compose to v2.10.1
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-19 16:46:17 +01:00
Nicolas De Loof
c8d687599a Fixed progress UI to adapt to terminal width
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-19 11:14:23 +01:00
Stavros Kois
2f108ffaa8 handle healthcheck.disable true in isServiceHealthy
Signed-off-by: Stavros Kois <s.kois@outlook.com>
2026-01-19 10:18:34 +01:00
Sebastiaan van Stijn
0a07df0e5b build(deps): bump github.com/sirupsen/logrus v1.9.4
full diff: https://github.com/sirupsen/logrus/compare/v1.9.3...v1.9.4

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-15 19:45:49 +01:00
tensorworker
02b606ef8e use go-compose instead Signed-off-by: tensorworker <tensorworker@proton.me>
Signed-off-by: tensorworker <tensorworker@proton.me>
2026-01-15 08:24:20 +01:00
tensorworker
9856802945 fix: expand tilde in --env-file paths to user home directory
When using --env-file=~/.env, the tilde was not expanded to the user's
home directory. Instead, it was treated as a literal character and
resolved relative to the current working directory, resulting in errors
like "couldn't find env file: /current/dir/~/.env".

This adds an ExpandUser function that expands ~ to the home directory
before converting relative paths to absolute paths.

Fixes #13508

Signed-off-by: tensorworker <tensorworker@proton.me>
2026-01-15 08:24:20 +01:00
Adam Sven Johnson
63ae7eb0fa Replace tabbed indentation in sdk.md
Tabs and spaces were mixed in the example code which didn't indent cleanly in the github preview.

Signed-off-by: Adam Sven Johnson <adam@pkqk.net>
2026-01-14 07:56:25 +01:00
dependabot[bot]
f17d0dfc61 build(deps): bump github.com/go-viper/mapstructure/v2
Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.4.0 to 2.5.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.4.0...v2.5.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 10:21:23 +01:00
dependabot[bot]
ef14cfcfea build(deps): bump google.golang.org/grpc from 1.77.0 to 1.78.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.77.0 to 1.78.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.77.0...v1.78.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 17:50:14 +01:00
hiroto.toyoda
b760afaf9f refactor: extract API version constants to dedicated file
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-11 17:04:40 +01:00
dependabot[bot]
a2a5c86f53 build(deps): bump golang.org/x/sys from 0.39.0 to 0.40.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.39.0 to 0.40.0.
- [Commits](https://github.com/golang/sys/compare/v0.39.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 10:22:48 +01:00
Sebastiaan van Stijn
98e82127b3 build(deps): bump github.com/containerd/containerd/v2 to v2.2.1
The pull request that was needed has been released now as part of v2.2.1;
full diff: https://github.com/containerd/containerd/compare/efd86f2b0bc2...v2.2.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-08 11:33:06 +01:00
Sebastiaan van Stijn
03e19e4a84 go.mod: remove exclude rules
Commit 640c7deae0 added these exclude
rules as a temporary workaround until these transitive dependency
versions would be gone;

> downgrade go-difflib and go-spew to tagged releases
>
> These dependencies were updated to "master" in some modules we depend on,
> but have no code-changes since their last release. Unfortunately, this also
> causes a ripple effect, forcing all users of the containerd module to also
> update these dependencies to an unrelease / un-tagged version.
>
> Both these dependencies will unlikely do a new release in the near future,
> so exclude these versions so that we can downgrade to the current release.

Kubernetes, and other dependencies have reverted those bumps, so these
exclude rules are no longer needed.

This reverts commit 640c7deae0.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-08 07:07:57 +01:00
Sebastiaan van Stijn
b2c17ff118 build(deps): bump github.com/klauspost/compress to v1.18.2
Fixes a regression in v1.18.1 that resulted in invalid flate/zip/gzip encoding.
The v1.18.1 tag has been retracted.

full diff: https://github.com/klauspost/compress/compare/v1.18.1...v1.18.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-07 14:03:12 +01:00
Nicolas De Loof
ec88588cd8 Removed build warning when no explicit build has been requested.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2026-01-05 13:17:20 +01:00
Jan-Robin Aumann-O'Keefe
7d5913403a add service name completion to down command
Signed-off-by: Jan-Robin Aumann-O'Keefe <jan-robin@aumann.org>
2026-01-05 09:32:39 +01:00
hiroto.toyoda
d95aa57f01 fix: avoid setting timeout when waitTimeout is not positive
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 09:31:14 +01:00
hiroto.toyoda
ee4c01b66b fix: correctly use errgroup.WithContext
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 09:27:46 +01:00
hiroto.toyoda
d7a65f53f8 fix: correct typo in isSwarmEnabled method name
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 09:11:58 +01:00
hiroto.toyoda
4520bcbaf6 fix: clean up temporary compose files after conversion
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 09:01:56 +01:00
hiroto.toyoda
327be1fcd5 add unit test
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2026-01-05 08:15:02 +01:00
Ignacio López Luna
59f04b85af remove duplicated version field
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Ignacio López Luna
b4574c8bd6 do not strip build metadata
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Ignacio López Luna
29d6c918c4 use github.com/docker/docker/api/types/versions for comparing versions and store plugin version obtained by pluginManager in newModelAPI
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Ignacio López Luna
58403169f3 Only append RuntimeFlags if docker model CLI version is >= v1.0.6
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Ignacio López Luna
6aee7f8370 gets back runtime flags when configuring models
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-12-18 15:24:06 +01:00
Nicolas De Loof
c89b8a2d6b warn user no service has been selected to build
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-18 11:35:33 +01:00
Nicolas De Loof
aec9f54176 check model plugin is successfully loaded and store version
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-18 11:01:35 +01:00
dependabot[bot]
232197d364 build(deps): bump github.com/moby/buildkit from 0.26.2 to 0.26.3
Bumps [github.com/moby/buildkit](https://github.com/moby/buildkit) from 0.26.2 to 0.26.3.
- [Release notes](https://github.com/moby/buildkit/releases)
- [Commits](https://github.com/moby/buildkit/compare/v0.26.2...v0.26.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 10:16:54 +01:00
dependabot[bot]
81ba889bee build(deps): bump tags.cncf.io/container-device-interface
Bumps [tags.cncf.io/container-device-interface](https://github.com/cncf-tags/container-device-interface) from 1.0.1 to 1.1.0.
- [Release notes](https://github.com/cncf-tags/container-device-interface/releases)
- [Changelog](https://github.com/cncf-tags/container-device-interface/blob/main/RELEASE.md)
- [Commits](https://github.com/cncf-tags/container-device-interface/compare/v1.0.1...v1.1.0)

---
updated-dependencies:
- dependency-name: tags.cncf.io/container-device-interface
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 09:34:43 +01:00
Nicolas De Loof
8e5b25c0f1 Restored support for BUILDKIT_PROGRESS.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-15 18:03:04 +01:00
Nicolas De Loof
d4c1987638 Prevented incorrect progress metrics to break compose display.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-15 18:02:31 +01:00
Nicolas De Loof
1297f97aef prefer aec library over raw ANSI sequences
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-15 16:36:57 +01:00
hiroto.toyoda
55cded1806 Avoid reassigning err variable
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2025-12-15 15:18:54 +01:00
hiroto.toyoda
6c043929a0 Fix missing error handling in setEnvWithDotEnv
Signed-off-by: hiroto.toyoda <hiroto.toyoda@dena.com>
2025-12-15 15:18:54 +01:00
Alexis Lefebvre
2750330566 doc: do not mention v2 on README
Signed-off-by: Alexis Lefebvre <alexislefebvre+github@gmail.com>
2025-12-15 11:46:38 +01:00
Nicolas De Loof
e22426443e Introduced fsnotify build tag to select watcher implementation
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-15 10:16:12 +01:00
Guillaume Lours
6599f8ad84 add 'configured' event at the end of model configuration phase
Currently when using models, the final message is 'confugiring' which could let users think the DMR configuration is still pending

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>

# Conflicts:
#	pkg/api/event.go
2025-12-15 10:14:21 +01:00
Nicolas De Loof
3853ad3911 prefer *task for memory efficiency updating tasks
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-12 14:22:02 +01:00
Nicolas De Loof
02008a0097 Restored image layer download progress details on pull.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-12 14:22:02 +01:00
dependabot[bot]
4f419e5098 build(deps): bump golang.org/x/sync from 0.18.0 to 0.19.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.18.0 to 0.19.0.
- [Commits](https://github.com/golang/sync/compare/v0.18.0...v0.19.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 17:13:15 +01:00
Nicolas De Loof
b62cbed87c Fixed status alignment in progress UI.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-11 15:25:03 +01:00
Nicolas De Loof
aa9a71f37a run finalization synchronously
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-10 22:49:37 +01:00
dependabot[bot]
ac211e6e51 build(deps): bump github.com/docker/cli-docs-tool from 0.10.0 to 0.11.0
Bumps [github.com/docker/cli-docs-tool](https://github.com/docker/cli-docs-tool) from 0.10.0 to 0.11.0.
- [Release notes](https://github.com/docker/cli-docs-tool/releases)
- [Commits](https://github.com/docker/cli-docs-tool/compare/v0.10.0...v0.11.0)

---
updated-dependencies:
- dependency-name: github.com/docker/cli-docs-tool
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-10 10:14:31 +01:00
Austin Vazquez
778a627b8e Set Go min version to absolute minimum version required
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
2025-12-09 20:33:00 +01:00
Austin Vazquez
359d2f076e ci: use .go-version file for actions/setup-go
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
2025-12-09 20:33:00 +01:00
Austin Vazquez
c9e0d83e14 ci: upgrade actions/setup-go from v5 to v6
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
2025-12-09 20:33:00 +01:00
dependabot[bot]
3e206fdcc6 build(deps): bump golang.org/x/sys from 0.38.0 to 0.39.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/sys/compare/v0.38.0...v0.39.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 16:18:09 +01:00
Nicolas De Loof
d12947e9f8 Fixed broken run --quiet.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-08 13:34:26 +01:00
xiaolinny
0878c59a74 chore: fix grammatical errors and improve clarity in code
Signed-off-by: xiaolinny <xiaolincode@outlook.com>
2025-12-08 11:20:48 +01:00
Nicolas De Loof
c0345e4f45 restore support for COMPOSE_COMPATIBILITY
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-07 18:42:04 +01:00
Nicolas De Loof
9fada6cc23 Bumped build images: tonistiigi/xx:1.9.0, crazymax/osxcross:15.5
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-05 15:19:02 +01:00
Rashmi Vagha
85ea24b62c Fix grammar: pluralize 'service' and remove apostrophes in lets
Signed-off-by: Rashmi Vagha <rvagha@umass.edu>
2025-12-05 10:33:35 +01:00
yangfeiyu
000a4a4b9f check buildx version before comparing it
Signed-off-by: yangfeiyu <yangfeiyu20102011@163.com>
2025-12-04 08:34:13 +01:00
Austin Vazquez
08de90c267 bump golang 1.24.11
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
2025-12-03 19:30:45 +01:00
Nicolas De Loof
cfcee45a89 fix SDK example
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-03 16:55:23 +01:00
zjumathcode
13d70b1c11 refactor: replace interface{} with any for clarity and modernization
Signed-off-by: zjumathcode <pai314159@2980.com>
2025-12-02 08:41:49 +01:00
Nicolas De Loof
72f4d655ef Bump compose go to v2.10.0
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-01 17:19:40 +01:00
Sebastiaan van Stijn
dc66e6bad1 golangci-lint: use gci formatter instead of goimports
Most files already grouped imports into "stdlib -> other -> local",
but some files didn't. The gci formatter is similar to goimports, but
has better options to make sure imports are grouped in the expected
order (and to make sure no additional groups are present).

This formatter has a 'fix' function, so code can be re-formatted auto-
matically;

    golangci-lint run -v --fix

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-12-01 12:21:50 +01:00
Rashmi Vagha
8d9d5259e0 Fix grammar: change 'adopted' to 'adopt' in Docker Swarm note
Signed-off-by: Rashmi Vagha <rvagha@umass.edu>
2025-12-01 12:08:52 +01:00
Nicolas De Loof
b32297dccd add --wait option to start command
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-12-01 11:58:00 +01:00
Ignacio López Luna
af8cac5768 just warn user
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-11-28 18:52:36 +01:00
Ignacio Lopez
8477a85ce6 feat(model): reject runtime flags in model configuration
Signed-off-by: Ignacio López Luna <ignasi.lopez.luna@gmail.com>
2025-11-28 18:52:36 +01:00
Sebastiaan van Stijn
6ee7146354 build(deps): bump golang.org/x/crypto v0.45.0
full diff: https://github.com/golang/crypto/compare/v0.44.0...v0.45.0

Hello gophers,

We have tagged version v0.45.0 of golang.org/x/crypto in order to address two
security issues.

This version fixes a vulnerability in the golang.org/x/crypto/ssh package and a
vulnerability in the golang.org/x/crypto/ssh/agent package which could cause
programs to consume unbounded memory or panic respectively.

SSH servers parsing GSSAPI authentication requests don't validate the number of
mechanisms specified in the request, allowing an attacker to cause unbounded
memory consumption.

Thanks to Jakub Ciolek for reporting this issue.

This is CVE-2025-58181 and Go issue https://go.dev/issue/76363.

SSH Agent servers do not validate the size of messages when processing new
identity requests, which may cause the program to panic if the message is
malformed due to an out of bounds read.

Thanks to Jakub Ciolek for reporting this issue.

This is CVE-2025-47914 and Go issue https://go.dev/issue/76364.

Cheers, Go Security team

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-28 15:44:55 +01:00
dependabot[bot]
f28503426c build(deps): bump github.com/hashicorp/go-version from 1.7.0 to 1.8.0
Bumps [github.com/hashicorp/go-version](https://github.com/hashicorp/go-version) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/hashicorp/go-version/releases)
- [Changelog](https://github.com/hashicorp/go-version/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/go-version/compare/v1.7.0...v1.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-28 10:13:47 +01:00
Nicolas De Loof
e0977c2df1 only check for env_file
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-28 09:48:55 +01:00
Nicolas De Loof
2d569916fe skip includes preparing publish
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-28 09:41:43 +01:00
liuyueyangxmu
3975f02153 refactor: use strings.Builder to improve performance
Signed-off-by: liuyueyangxmu <liuyueyangxmu@outlook.com>
2025-11-26 10:52:24 +01:00
Nicolas De Loof
fa832d72d7 Added support for build.no_cache_filter
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-26 09:37:04 +01:00
vicerace
822f5a702b refactor: replace Split in loops with more efficient SplitSeq
Signed-off-by: vicerace <vicerace@sohu.com>
2025-11-25 11:37:03 +01:00
Nicolas De Loof
68bb7a71ba bump dependencies
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-24 17:26:40 +00:00
Nicolas De Loof
6f365395e5 Fix support for port range
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-16 20:38:54 +01:00
Sebastiaan van Stijn
3052934624 build(deps): bump github.com/docker/buildx from v0.29.1 to v0.30.0
full diff: https://github.com/docker/buildx/compare/v0.29.1...v0.30.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-16 14:55:26 +01:00
Sebastiaan van Stijn
428abab16a build(deps): bump github.com/moby/buildkit from v0.25.2 to v0.26.0
full diff: https://github.com/moby/buildkit/compare/v0.25.2...v0.26.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-16 14:55:26 +01:00
Sebastiaan van Stijn
755618e707 build(deps): bump go.opentelemetry.io/otel v1.38.0, go.opentelemetry.io/contrib v0.63.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-16 14:55:26 +01:00
Sebastiaan van Stijn
c47b8c32e3 Dockerfile: update golangci-lint to v2.6.2
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-16 14:54:58 +01:00
Sebastiaan van Stijn
89d3944837 fix linting issues
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-16 14:54:58 +01:00
Sebastiaan van Stijn
f2b14fe1aa gha: use custom names for matrix
Manually enumerate the combinations ((plugin|standalone), (version))
so that we can assign a predictable name ("stable", "oldstable") and
prevent having to update the branch-protection rules for each update
to mark the tests as required.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-16 14:42:03 +01:00
Sebastiaan van Stijn
bd2257b6d1 gha: test against docker v29, v28
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-16 14:42:03 +01:00
Nicolas De Loof
d7e5f20eb6 images command should display image Created time or N/A if not available
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-13 09:40:56 +01:00
Nicolas De Loof
2b4543935c next release will be major version v5.x
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-13 09:32:43 +01:00
aevesdocker
f0dce1b977 sdk docs: patch
Signed-off-by: aevesdocker <allie.sadler@docker.com>
2025-11-12 16:24:24 +01:00
Nicolas De Loof
6e55832b1c add (restore) support for detach keys
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-12 14:23:58 +01:00
Nicolas De Loof
45def51117 make DRYRUN_PREFIX a display attribute, move DryRunClient out of pkg/api
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-12 11:17:28 +01:00
Nicolas De Loof
aff5c115d6 move progress UI components into cmd
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-12 11:17:28 +01:00
Nicolas De Loof
5ef495c898 removed unecessary check
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-12 08:34:29 +01:00
Nicolas De Loof
9de7e2a388 SDK docs
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-10 16:15:36 +01:00
Jonty
dc90c4e44d Grammatical fix
Signed-off-by: Jonty <jontyleslie@gmail.com>
2025-11-10 11:34:18 +01:00
Jonty
91e1753d80 Grammatical fix
Signed-off-by: Jonty <jontyleslie@gmail.com>
2025-11-10 11:34:18 +01:00
Jonty
9db27a65c6 Making the American/British spellings consistent to the error messages
Signed-off-by: Jonty <jontyleslie@gmail.com>
2025-11-10 11:34:18 +01:00
dependabot[bot]
efd7424da7 build(deps): bump golang.org/x/sync from 0.17.0 to 0.18.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.17.0 to 0.18.0.
- [Commits](https://github.com/golang/sync/compare/v0.17.0...v0.18.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 11:09:20 +01:00
dependabot[bot]
02109c8d33 build(deps): bump golang.org/x/sys from 0.37.0 to 0.38.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/sys/compare/v0.37.0...v0.38.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 10:23:37 +01:00
Nicolas De Loof
c37ede62db on up buildOptions must include all enabled services
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-06 16:28:46 +01:00
Nicolas De Loof
7eb5adeef6 introduce --insecure-registry, reserved for testing purpose
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-06 16:17:21 +01:00
Nicolas De Loof
0793ad7c68 document support for OCI and Git remote resources
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-06 16:14:05 +01:00
dependabot[bot]
8137b2bce8 build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.5.1+incompatible to 28.5.2+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.5.1...v28.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-06 10:27:37 +01:00
dependabot[bot]
4e3372b473 build(deps): bump github.com/containerd/containerd/v2
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.4 to 2.2.0.
- [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.4...v2.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-06 10:16:04 +01:00
dependabot[bot]
fef26fb372 build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.5.1+incompatible to 28.5.2+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.1...v28.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-06 10:15:05 +01:00
dependabot[bot]
a32e13a2b0 build(deps): bump github.com/moby/buildkit from 0.25.1 to 0.25.2
Bumps [github.com/moby/buildkit](https://github.com/moby/buildkit) from 0.25.1 to 0.25.2.
- [Release notes](https://github.com/moby/buildkit/releases)
- [Commits](https://github.com/moby/buildkit/compare/v0.25.1...v0.25.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-06 10:14:25 +01:00
Suleiman Dibirov
67e39a41f2 fixes
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-11-06 08:02:59 +01:00
Suleiman Dibirov
dc1283289d fix: use NewParallelCLI in compose_run_build_once_test.go
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-11-06 08:02:59 +01:00
dependabot[bot]
0c596ed3cf build(deps): bump github.com/containerd/platforms
Bumps [github.com/containerd/platforms](https://github.com/containerd/platforms) from 1.0.0-rc.1 to 1.0.0-rc.2.
- [Release notes](https://github.com/containerd/platforms/releases)
- [Commits](https://github.com/containerd/platforms/compare/v1.0.0-rc.1...v1.0.0-rc.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-05 17:43:11 +01:00
Nicolas De Loof
13870006fb disable progress UI when build is ran with --print
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-04 17:30:56 +01:00
Nicolas De Loof
af579ebd4b drop support for internal buildkit builder
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-04 11:36:40 +01:00
Nicolas De Loof
fc2a7d13fa from Compose CLI, we know the streams used to configure LogConsumer
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-04 10:25:14 +01:00
Nicolas De Loof
d70bb8cf5e distinguish event (short) status text and details
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-04 10:18:55 +01:00
Nicolas De Loof
bff3d35305 render events in order they were first received
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-11-04 10:18:55 +01:00
Guillaume Lours
b80bb0586e Migrate CLI commands to use LoadProject API
Simplifying the codebase and eliminating duplicate backend creation.

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-11-03 08:58:30 +01:00
Guillaume Lours
d74274bc04 Add LoadProject method to Compose SDK API
This commit adds a new LoadProject method to the Compose service API,
allowing SDK users to programmatically load Compose projects with full
control over the loading process.

Changes:

1. New API method (pkg/api/api.go):
   - LoadProject(ctx, ProjectLoadOptions) (*types.Project, error)
   - ProjectLoadOptions struct with all loader configuration
   - LoadListener callback for event notifications (metrics, etc.)
   - ProjectOptionsFns field for compose-go loader options

2. Implementation (pkg/compose/loader.go):
   - createRemoteLoaders: Git and OCI remote loader setup
   - buildProjectOptions: Translates ProjectLoadOptions to compose-go options
   - postProcessProject: Service filtering, labels, resource pruning

3. Unit test (pkg/compose/loader_test.go):
   - Tests basic project loading functionality
   - Verifies ProjectOptionsFns with cli.WithoutEnvironmentResolution

4. Mock update (pkg/mocks/mock_docker_compose_api.go):
   - Added LoadProject to mock interface

Key design decisions:
- LoadListener pattern keeps metrics collection in CLI, not SDK
- ProjectOptionsFns exposes compose-go options directly (e.g., cli.WithInterpolation(false))
- Post-processing in SDK: labels, service filtering, resource pruning
- Environment resolution NOT in SDK (command responsibility)
- Compatibility mode handling (api.Separator)

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-11-03 08:58:30 +01:00
Stanislav Zhuk
10f15cacdd fix typo in error message
Signed-off-by: Stanislav Zhuk <stasadev@gmail.com>
2025-11-03 08:38:49 +01:00
Guillaume Lours
3658a063bb add AlwaysOkPrompt to replace 'AlwaysYes' current implementation'
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-11-03 08:01:59 +01:00
Sebastiaan van Stijn
74a4ccdd85 fix various linting issues
Got these when running locally on a more recent version of golangci-lint:

    pkg/compose/build_bake.go:187:3: importShadow: shadow of imported from 'github.com/docker/cli/cli/command/image/build' package 'build' (gocritic)
                    build := *service.Build
                    ^
    pkg/compose/build_bake.go:526:19: importShadow: shadow of imported from 'github.com/docker/cli/cli/command/image/build' package 'build' (gocritic)
    func toBakeAttest(build types.BuildConfig) []string {
                      ^
    pkg/compose/create.go:1453:2: importShadow: shadow of imported from 'github.com/docker/docker/api/types/network' package 'network' (gocritic)
            network string,
            ^
    pkg/compose/create.go:1468:2: importShadow: shadow of imported from 'github.com/docker/docker/api/types/network' package 'network' (gocritic)
            network string,
            ^
    pkg/compose/monitor.go:42:17: importShadow: shadow of imported from 'github.com/docker/compose/v2/pkg/api' package 'api' (gocritic)
    func newMonitor(api client.APIClient, project string) *monitor {
                    ^
    cmd/compose/config.go:337:1: File is not properly formatted (gofumpt)
            return
    ^
    pkg/compose/convergence.go:608:1: File is not properly formatted (gofumpt)
                    return
    ^
    pkg/compose/cp.go:335:1: File is not properly formatted (gofumpt)
                    return
    ^
    pkg/e2e/compose_up_test.go:35:10: go-require: c.RunDockerComposeCmd contains assertions that must only be used in the goroutine running the test function (testifylint)
                    res := c.RunDockerComposeCmd(t, "-f", "fixtures/dependencies/deps-completed-successfully.yaml", "--project-name", projectName, "up", "--wait", "-d")
                           ^
    pkg/e2e/healthcheck_test.go:42:10: go-require: c.RunDockerComposeCmd contains assertions that must only be used in the goroutine running the test function (testifylint)
                    res := c.RunDockerComposeCmd(t, "-f", "fixtures/start_interval/compose.yaml", "--project-name", projectName, "up", "--wait", "-d", "test")
                           ^
    10 issues:
    * gocritic: 5
    * gofumpt: 3
    * testifylint: 2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-10-31 17:44:38 +01:00
Guillaume Lours
6719f47bd4 test checking bake internal load build definition
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-30 21:25:32 +01:00
Guillaume Lours
3eb2934eb7 bump compose-go to version v2.9.1
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-30 10:12:50 +01:00
Suleiman Dibirov
c416ea7036 fix compose_run_build_once_test.go
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-10-30 10:08:29 +01:00
Suleiman Dibirov
0d396bbacb fix(git): Add validation for Git subdirectory paths to prevent traversal
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-10-30 10:08:29 +01:00
Suleiman Dibirov
fc74c78963 Update e2e tests in compose_run_build_once_test.go to use project names for Docker Compose commands.
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-10-29 10:47:18 +01:00
Suleiman Dibirov
658bff335f Revert "no parallel in compose_run_build_once_test.go"
This reverts commit e4f4a5aa86.

Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-10-29 10:47:18 +01:00
Suleiman Dibirov
80030e1390 no parallel in compose_run_build_once_test.go
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-10-29 10:47:18 +01:00
Suleiman Dibirov
6a35be5112 lint fix
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-10-29 10:47:18 +01:00
Suleiman Dibirov
0c854a6ab7 add e2e tests
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-10-29 10:47:18 +01:00
Suleiman Dibirov
557e0b6ec7 fix(run): Ensure images exist only for the target service in run command
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-10-29 10:47:18 +01:00
Nicolas De Loof
a8933c91e7 stop progress UI during build to prevent interference with buildkit Display
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-29 10:11:30 +01:00
Nicolas De Loof
7e3993bcac skip Start[ed|ing] events to avpd mix with container logs
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-29 10:11:30 +01:00
Nicolas De Loof
fd4f2f99cf register TTYWritter as an Event Processor
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-29 10:11:30 +01:00
Nicolas De Loof
ae25d27e5a remove unused RunWithStatus, always pass operation as title
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-29 10:11:30 +01:00
Nicolas De Loof
394466683a use eventBus to collect tasks progress
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-29 10:11:30 +01:00
Nicolas De Loof
e5c8b68642 decouple Event from tty progress writer
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-29 10:11:30 +01:00
Nicolas De Loof
bf50c99193 pretend cli.Out is a containerd console.File
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-28 14:39:50 +01:00
Guillaume Lours
8274be8d08 configure Compose service with io.Reader and io.Writer
remove usage of internal IO interfaces

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-27 18:54:10 +01:00
Guillaume Lours
86e91e010d Add streamOverrideWrapper to intercepts command.Cli stream methods and transparently returns custom streams when provided via options
Add new GetConfiguredStreams function to Compose API definition

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-27 16:17:50 +01:00
Guillaume Lours
e1678c5c43 Introduce abstractions to support SDK usage without requiring Docker CLI
This commit prepares the Compose service for SDK usage by abstracting away
the hard dependency on command.Cli. The Docker CLI remains the standard path
for the CLI tool, but SDK users can now provide custom implementations of
streams and context information.

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-27 16:17:50 +01:00
Nicolas De Loof
5924387e89 run hooks on restart
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-27 10:24:18 +01:00
Nicolas De Loof
7f668bd7fe Setup Compose service using functional parameters
This commit introduces WithMaxConcurrency and WithDryRun to replace direct mutators on composeService
commands and flags are translated into a set of functional parameters which are eventually applied
as a ComposeService is created just before being actually used by a command

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-24 18:24:21 +02:00
Nicolas De Loof
3ce52883cb prompt default implementation to prevent a panic
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-24 10:21:59 +02:00
Nicolas De Loof
ac3b8fd8a5 Code Cleanup
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-24 10:05:59 +02:00
Anton Ovchinnikov
8619f5d72a Fix help output for "exec --no-tty" option
Signed-off-by: Anton Ovchinnikov <anton@tonyo.info>
2025-10-23 18:19:41 +02:00
Nicolas De Loof
e59150baa8 fix OCI compose override support
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-23 12:04:37 +02:00
Nicolas De Loof
6a90742ef2 Test to check writeComposeFile detects invalid OCI artifact
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-22 20:09:32 +02:00
Nicolas De Loof
6007d4c7e7 publish env_file references as opaque hash to prevent paths conflicts
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-22 19:32:16 +02:00
Guillaume Lours
69bcb962bf Enforce compose files from OCI artifact all get into the same target (cache) folder
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-22 19:32:16 +02:00
Nicolas De Loof
9b4fcce034 introduce WithPrompt to configure compose backend to use a plugable UI component for user interaction
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-22 16:14:55 +02:00
Guillaume Lours
da5c57c29d test digest or canonical reference, not only tag, when checking if an image is already present
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-20 18:42:24 +02:00
Nicolas De Loof
e25265dd55 remove unused code to only rely on api.Service
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-20 14:53:04 +02:00
Nicolas De Loof
e19e1278b5 fail build if minimal required version of buildx isn't installed
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-20 12:25:38 +02:00
Nicolas De Loof
585c4db4f9 Compose can't create a tar with adequate uid:gid ownership
as we can't get container UID/GID as int by ContainerInspect
revert https://github.com/docker/compose/pull/13288

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-20 09:51:58 +02:00
Guillaume Lours
be8c7e6c60 make CTRL+Z a no-op operation on Windows
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-17 15:07:37 +02:00
Nicolas De Loof
27f59d7f42 Detect failure to access os.TempDir
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-17 09:49:44 +02:00
Nicolas De Loof
2681ed17a7 mutualize code from injectSecrets / injectConfigs
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-16 17:43:04 +02:00
Nicolas De Loof
ee75be342b Set secret/config uid:gid to match container's USER
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-16 17:43:04 +02:00
Paul Thiele
157617480a fix race-condition bug in publish command
Signed-off-by: Paul Thiele <paul.thiele@kinexon.com>
2025-10-16 09:24:57 +02:00
Nicolas De Loof
88aae9c46e support Ctrl+Z to run compose in background
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-15 17:26:11 +02:00
Guillaume Lours
7755302348 use fixed version of compose bridge transformer images
to avoid CI issue on Compose when a new version is released and change the outputs

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-14 09:21:59 +02:00
Guillaume Lours
147923c44c bump golang to version 1.24.9
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-10-14 09:14:56 +02:00
Olivier Goulpeau
289faae5fa fix(publish): in publish(), select all profiles in the project to publish.
This code is moved from `generateImageDigestsOverride()` as no more
needed at that point.

Signed-off-by: Olivier Goulpeau <olivier.goulpeau@ledger.fr>
2025-10-14 08:25:26 +02:00
Olivier Goulpeau
e7aa484b78 fix(publish): in processFile(), load the compose file passing the project.Profiles to the loader.Options.
Signed-off-by: Olivier Goulpeau <olivier.goulpeau@ledger.fr>
2025-10-14 08:25:26 +02:00
Sebastiaan van Stijn
ae3309afab pkg/compose: build with bake: drop support for buildx v0.16 and lower
[buildx v0.17][1] was released a Year ago, so any version this
conditional code was accounting for would be versions before that;
the latest of which being [buildx v0.16.2][2] (July 2024).

Given that those versions are long EOL and no longer supported, we
can probably remove the conditional code.

[1]: https://github.com/docker/buildx/releases/tag/v0.17.0
[2]: https://github.com/docker/buildx/releases/tag/v0.16.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-10-14 08:02:58 +02:00
Sebastiaan van Stijn
0b5fb36eb5 build(deps): bump docker/buildx v0.29.1, moby/buildkit v0.25.1
full diff:

- https://github.com/docker/buildx/compare/v0.28.0...v0.29.1
- https://github.com/moby/buildkit/compare/v0.24.0...v0.25.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-10-13 21:21:01 +02:00
Sebastiaan van Stijn
63920c4cc0 pkg/compose: align classic builder implementation with docker/cli
This aligns the implementation closer to the implementation in docker/cli,
with the refactor done in [cli@260f1db]; this removes some direct uses of
the github.com/docker/docker/builder/remotecontext/urlutil package, which
won't be included in the new Moby modules.

There's still some remaining uses in the `dockerFilePath` utility (which
may need to be updated to also account for remote contexts that are not
"git"), so possibly we can remove the use in that utility as well.

[cli@260f1db]: 260f1dbebb

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-10-13 16:53:00 +02:00
Nicolas De Loof
a03f2562df bake only interpolates ${*}
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-13 10:40:19 +02:00
dependabot[bot]
a07f2b8ded build(deps): bump golang.org/x/sys from 0.36.0 to 0.37.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.36.0 to 0.37.0.
- [Commits](https://github.com/golang/sys/compare/v0.36.0...v0.37.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 11:35:31 +02:00
dependabot[bot]
f45a3ebcfd build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.5.0+incompatible to 28.5.1+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.5.0...v28.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 11:35:14 +02:00
dependabot[bot]
7fec70b6c7 build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.5.0+incompatible to 28.5.1+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.0...v28.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-09 11:34:52 +02:00
Kian Eliasi
ce463d50b2 Fix: set PWD only if not set
Signed-off-by: Kian Eliasi <kian.elbo@gmail.com>
2025-10-08 10:12:02 +02:00
Benedikt Radtke
fa7e85ed83 Write error to watcher error channel if Start() fails
Up's loop will notice globalCtx is done, and invoke watcher.Stop(). Stop() reads from the watcher error channel. If Start() does not write an error, Stop() will never finish.

Fixes https://github.com/docker/compose/issues/13262

Signed-off-by: Benedikt Radtke <benediktradtke@gmail.com>
2025-10-06 14:50:19 +02:00
dependabot[bot]
d9423f6872 build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.5.0-rc.1+incompatible to 28.5.0+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.5.0-rc.1...v28.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-03 11:32:15 +02:00
dependabot[bot]
5add90240d build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.5.0-rc.1+incompatible to 28.5.0+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.5.0-rc.1...v28.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-03 11:32:08 +02:00
Nicolas De Loof
07602f2070 publish Compose application as compose.yaml + images
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-03 10:59:57 +02:00
Nicolas De Loof
cf7e31f731 escape $ in bake.json as interpolation already has been managed by compose
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-10-03 09:25:35 +02:00
Nicolas De Loof
fa08127456 use containerd client for OCI operations
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-30 12:03:46 +02:00
Nicolas De Loof
4ee52ad168 pass bake secrets by env
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-30 09:07:22 +02:00
Sebastiaan van Stijn
4a4776ec57 cmd/compose: fix minor linting issues
- inline variable that shadowed package-type
- don't use apiBuildOptions if an error was returned

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-29 15:25:22 +02:00
Sebastiaan van Stijn
713de5bb9e pkg/compose: explicitly map AuthConfig fields instead of a direct cast
Commit [cli@27b2797] forked the AuthConfig type from the API, and changed
existing code to do a direct cast / convert of the forked type to the API
type. This can cause issues if the API types diverges, such as the removal
of the Email field.

This patch explicitly maps each field to the corresponding API type, but
adds some TODOs, because various code-paths only included a subset of the
fields, which may be intentional for fields that were meant to be handled
on the daemon / registry-client only.

We should evaluate these conversions to make sure these fields should
be sent from the client or not (and possibly even removed from the API
type).

[cli@27b2797]: 27b2797f7d

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-29 15:24:42 +02:00
Sebastiaan van Stijn
9ded1684cd gha: update test-matrix: remove docker 26.x
- Mirantis Container Runtime (MCR) 23.0 reached EOL, and the next LTS
  version of MCR is 25.x, but download.docker.com does not have 25.x
  packages for the latest Ubuntu release.
- Docker 26.x reached EOL and is no longer maintained

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-29 14:27:41 +02:00
Guillaume Lours
8bc8593fd0 provider services: use '--project-name=' notation
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-29 12:38:10 +02:00
Nicolas De Loof
8978c1027d use containerd registry client
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-26 18:45:07 +02:00
Sebastiaan van Stijn
032e0309ee cmd: pluginMain: use WithUserAgent option
Rewrite the custom user agent to use the new options that were added
in the cli:

- plugin.Run now accepts custom CLI options to allow customizing the CLI's
- cli/command now has a WithUserAgent option to customize the CLI's user-
  agent, allowing it to be overridden from the default.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-25 16:50:44 +02:00
Sebastiaan van Stijn
38ba35e165 pkg/mocks: re-generate mocks
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-25 16:50:44 +02:00
Sebastiaan van Stijn
56e0ba8080 build(deps): bump github.com/docker/docker, docker/cli v28.5.0-rc.1
full diff:

- https://github.com/docker/cli/compare/v28.4.0...v28.5.0-rc.1
- https://github.com/docker/docker/compare/v28.4.0...v28.5.0-rc.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-25 16:50:44 +02:00
Sebastiaan van Stijn
9752fa5500 pluginMain: remove uses of DockerCLI.Apply
The Apply method was added when CLI options for constructing the CLI were
rewritten into functional options in [cli@7f207f3]. There was no mention
in the pull request of this method specifically, and we want to remove or
reduce functions that mutate the CLI configuration after initialization
if possible (and likely remove the `Apply` method).

This patch removes the use of the `Apply` function as an intermediate step;
improvements will be made in the CLI itself for a more solid implementation.

[cli@7f207f3]: 7f207f3f95

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-24 14:09:55 +02:00
Sebastiaan van Stijn
4761fd88b0 pkg/compose: build: remove permissions warning on Windows
This warning was added in [moby@4a8b3ca] to print a warning when building
Linux images from a Windows client. Window's filesystem does not have an
"executable" bit, which mean that, for example, copying a shell script
to an image during build would lose the executable bit. So for Windows
clients, the executable bit would be set on all files, unconditionally.

Originally this was detected in the client, which had direct access to
the API response headers, but when refactoring the client to use a common
library in [moby@535c4c9], this was refactored into a `ImageBuildResponse`
wrapper, deconstructing the API response into an `io.Reader` and a string
field containing only the `OSType` header.

This was the only use and only purpose of the `OSType` field, and now that
BuildKit is the default builder for Linux images, this warning didn't get
printed unless BuildKit was explicitly disabled.

This patch removes the warning, so that we can potentially remove the
field, or the `ImageBuildResponse` type altogether.

[moby@4a8b3ca]: 4a8b3cad60
[moby@535c4c9]: 535c4c9a59

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-24 10:26:05 +02:00
Sebastiaan van Stijn
02c8e63545 pkg/watch: remove unused IsWindowsShortReadError
This function was added in b3615d64e2 but
appears to be unused.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-24 09:29:27 +02:00
Sebastiaan van Stijn
ab7a6e9322 pkg/compose: remove uses of deprecated mitchellh/mapstructure module
The github.com/mitchellh/mapstructure module was archived and is no longer
maintained. This module has moved to github.com/go-viper/mapstructure,
which updated to v2, with a minor breaking change in v2.0;

> Error is removed in favor of errors.Join (backported from Go 1.20 to
> preserve compatibility with earlier versions)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-24 09:21:40 +02:00
Nicolas De Loof
2ca7b96e33 resolve secrets based on env var before executing bake
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-22 14:56:15 +02:00
Ricardo Branco
a32dc3da72 test: Set stop_signal to SIGTERM
The official nginx images set STOPSIGNAL to SIGQUIT which dumps core.
Set it to SIGTERM to avoid dumping core on e2e tests.

Signed-off-by: Ricardo Branco <rbranco@suse.de>
2025-09-19 10:31:20 +02:00
Guillaume Lours
db260938c1 bump compose-go to version v2.9.0
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-19 10:13:08 +02:00
Guillaume Lours
5aea94794c Update comment on run command with tty & piped command
Co-authored-by: Nicolas De loof <nicolas.deloof@gmail.com>
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-19 09:51:15 +02:00
Guillaume Lours
d07c437ce8 dectect if piped run command and disable tty if so
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-19 09:51:15 +02:00
Guillaume Lours
da72230c39 remove tty attribute from run options and use dedicated variable to avoid confusion
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-19 09:51:15 +02:00
Nicolas De Loof
a429c09dfa fix support for build with bake when target docker endpoint requires TLS
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-18 11:16:47 +02:00
Guillaume Lours
df3c27c864 add deprecation warning for x-initialSync + e2e test
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-18 11:06:44 +02:00
Guillaume Lours
956891af54 add support of develop.watch.initial_sync attribute
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-18 11:06:44 +02:00
Nicolas De Loof
a473341058 volume ls command can run without a project
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-10 09:41:47 +02:00
Guillaume Lours
385b3f5c96 bump compose-go to version v2.8.2
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-08 18:07:00 +02:00
Nicolas De Loof
2d482e61ce propagate docker endpoint to bake using DOCKER_* env variables
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 17:54:55 +02:00
Nicolas De loof
c75418ee07 Apply suggestions from code review
Co-authored-by: Allie Sadler <102604716+aevesdocker@users.noreply.github.com>
Signed-off-by: Nicolas De loof <nicolas.deloof@gmail.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 15:56:55 +02:00
Nicolas De Loof
0cdc5c9bff rename --no-TTY => --no-tty for consistency
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 15:56:55 +02:00
Nicolas De Loof
b768232c0e document (hidden) --tty --interactive flags
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 15:56:55 +02:00
Nicolas De Loof
09689400e5 fix run --build support for service:* reference in additional_context
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 15:10:29 +02:00
Nicolas De Loof
cb3691154b detect container is restarted
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 15:03:18 +02:00
Nicolas De Loof
b387ba4a05 only load COMPOSE_* from $PWD/.env
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 14:40:49 +02:00
Nicolas De Loof
7cd569922e only propagate os.Env to bake, not the whole project.Environment
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 14:40:49 +02:00
Nicolas De Loof
eec2bb7ea6 only force plain mode build if progress is set to auto
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-08 14:17:04 +02:00
dependabot[bot]
2c15aef2ed build(deps): bump golang.org/x/sys from 0.35.0 to 0.36.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/sys/compare/v0.35.0...v0.36.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 13:49:29 +02:00
dependabot[bot]
290366205b build(deps): bump golang.org/x/sync from 0.16.0 to 0.17.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/sync/compare/v0.16.0...v0.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 13:33:10 +02:00
Guillaume Lours
a91ca95a71 bump golang to version 1.24.7
to align with moby/moby version

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-09-08 12:05:41 +02:00
Sebastiaan van Stijn
beb81a73f9 pkg/compose: remove aliases for container-state consts
These are no longer used, and have no known external consumers.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-08 11:52:28 +02:00
Sebastiaan van Stijn
f217207876 pkg/compose: use state consts from moby API
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-08 11:52:28 +02:00
Nicolas De Loof
02ffe2ac6c prefer application container vs one-off running exec without index
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-07 14:41:34 +02:00
dependabot[bot]
f48131fb66 build(deps): bump github.com/spf13/pflag from 1.0.9 to 1.0.10
Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.9 to 1.0.10.
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.9...v1.0.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-07 14:37:04 +02:00
Nicolas De Loof
4dd369bdcb fix sigint/sigterm support in logs --follow
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-09-07 14:19:56 +02:00
dependabot[bot]
ad73766bf2 build(deps): bump github.com/docker/buildx from 0.28.0-rc2 to 0.28.0
Bumps [github.com/docker/buildx](https://github.com/docker/buildx) from 0.28.0-rc2 to 0.28.0.
- [Release notes](https://github.com/docker/buildx/releases)
- [Commits](https://github.com/docker/buildx/compare/v0.28.0-rc2...v0.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 08:38:23 +02:00
Sebastiaan van Stijn
3c1f5a1815 go.mod: bump github.com/docker/docker, docker/cli v28.4.0
full diffs:

- https://github.com/docker/docker/compare/v28.3.3...v28.4.0
- https://github.com/docker/cli/compare/v28.3.3...v28.4.0
- https://github.com/moby/buildkit/compare/v0.24.0-rc2...v0.24.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-04 15:23:59 +02:00
dependabot[bot]
42d1e4c333 build(deps): bump github.com/spf13/cobra from 1.9.1 to 1.10.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.9.1 to 1.10.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-03 14:44:38 +02:00
dependabot[bot]
6ca8663bda build(deps): bump github.com/spf13/pflag from 1.0.7 to 1.0.9
Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.7 to 1.0.9.
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.7...v1.0.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 15:46:14 +02:00
Sebastiaan van Stijn
b33ecf65e8 go.mod: bump buildx v0.28.0-rc2, buildkit v0.24.0-rc2
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-09-02 07:28:06 +02:00
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
287 changed files with 8832 additions and 5784 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

@@ -22,24 +22,6 @@ permissions:
contents: read # to fetch code (actions/checkout)
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Create matrix
id: platforms
run: |
echo matrix=$(docker buildx bake binary-cross --print | jq -cr '.target."binary-cross".platforms') >> $GITHUB_OUTPUT
-
name: Show matrix
run: |
echo ${{ steps.platforms.outputs.matrix }}
validate:
runs-on: ubuntu-latest
strategy:
@@ -63,63 +45,88 @@ jobs:
make ${{ matrix.target }}
binary:
uses: docker/github-builder/.github/workflows/bake.yml@v1
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) with GitHub OIDC Token
with:
runner: amd64
artifact-name: compose
artifact-upload: true
cache: true
cache-scope: binary
target: release
output: local
sbom: true
sign: ${{ github.event_name != 'pull_request' }}
binary-finalize:
runs-on: ubuntu-latest
needs:
- prepare
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
- binary
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Prepare
run: |
platform=${MATRIX_PLATFORM}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
env:
MATRIX_PLATFORM: ${{ matrix.platform }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build
uses: docker/bake-action@v6
name: Download artifacts
uses: actions/download-artifact@v7
with:
source: .
targets: release
provenance: mode=max
sbom: true
set: |
*.platform=${{ matrix.platform }}
*.cache-from=type=gha,scope=binary-${{ env.PLATFORM_PAIR }}
*.cache-to=type=gha,scope=binary-${{ env.PLATFORM_PAIR }},mode=max
path: /tmp/compose-output
name: ${{ needs.binary.outputs.artifact-name }}
-
name: Rename provenance and sbom
run: |
for pdir in /tmp/compose-output/*/; do
(
cd "$pdir"
binname=$(find . -name 'docker-compose-*')
filename=$(basename "${binname%.exe}")
mv "provenance.json" "${filename}.provenance.json"
mv "sbom-binary.spdx.json" "${filename}.sbom.json"
find . -name 'sbom*.json' -exec rm {} \;
if [ -f "provenance.sigstore.json" ]; then
mv "provenance.sigstore.json" "${filename}.sigstore.json"
fi
)
done
mkdir -p "./bin/release"
mv /tmp/compose-output/**/* "./bin/release/"
-
name: Create checksum file
working-directory: ./bin/release
run: |
binname=$(find . -name 'docker-compose-*')
filename=$(basename "$binname" | sed -E 's/\.exe$//')
mv "provenance.json" "${filename}.provenance.json"
mv "sbom-binary.spdx.json" "${filename}.sbom.json"
find . -name 'sbom*.json' -exec rm {} \;
-
name: List artifacts
run: |
tree -nh ./bin/release
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
mv $RUNNER_TEMP/checksums.txt .
cat checksums.txt | while read sum file; do
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json && "${file#\*}" != *.sigstore.json ]]; then
echo "$sum $file" > ${file#\*}.sha256
fi
done
-
name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: compose-${{ env.PLATFORM_PAIR }}
path: ./bin/release
name: release
path: ./bin/release/*
if-no-files-found: error
bin-image-test:
if: github.event_name == 'pull_request'
uses: docker/github-builder/.github/workflows/bake.yml@v1
with:
runner: amd64
target: image-cross
cache: true
cache-scope: bin-image-test
output: image
push: false
sbom: true
set-meta-labels: true
meta-images: |
compose-bin
meta-tags: |
type=ref,event=pr
meta-bake-target: meta-helper
test:
runs-on: ubuntu-latest
steps:
@@ -147,18 +154,29 @@ jobs:
with:
paths: bin/coverage/unit/report.xml
if: always()
e2e:
runs-on: ubuntu-latest
name: e2e (${{ matrix.mode }}, ${{ matrix.channel }})
strategy:
fail-fast: false
matrix:
mode:
- plugin
- standalone
engine:
- 26
- 27
- 28
include:
# current stable
- mode: plugin
engine: 29
channel: stable
- mode: standalone
engine: 29
channel: stable
# old stable (latest major - 1)
- mode: plugin
engine: 28
channel: oldstable
- mode: standalone
engine: 28
channel: oldstable
steps:
- name: Prepare
run: |
@@ -189,9 +207,9 @@ jobs:
docker model version
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
go-version-file: '.go-version'
check-latest: true
cache: true
@@ -244,6 +262,7 @@ jobs:
with:
paths: /tmp/report/report.xml
if: always()
coverage:
runs-on: ubuntu-latest
needs:
@@ -254,9 +273,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
go-version-file: '.go-version'
check-latest: true
- name: Download unit test coverage
uses: actions/download-artifact@v4
@@ -280,40 +299,26 @@ jobs:
path: ./coverage.txt
if-no-files-found: error
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
release:
permissions:
contents: write # to create a release (ncipollo/release-action)
runs-on: ubuntu-latest
needs:
- binary
- binary-finalize
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
pattern: compose-*
path: ./bin/release
merge-multiple: true
-
name: Create checksums
working-directory: ./bin/release
run: |
find . -type f -print0 | sort -z | xargs -r0 shasum -a 256 -b | sed 's# \*\./# *#' > $RUNNER_TEMP/checksums.txt
shasum -a 256 -U -c $RUNNER_TEMP/checksums.txt
mv $RUNNER_TEMP/checksums.txt .
cat checksums.txt | while read sum file; do
if [[ "${file#\*}" == docker-compose-* && "${file#\*}" != *.provenance.json && "${file#\*}" != *.sbom.json ]]; then
echo "$sum $file" > ${file#\*}.sha256
fi
done
name: release
-
name: List artifacts
run: |

View File

@@ -33,9 +33,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
go-version-file: '.go-version'
cache: true
check-latest: true
@@ -74,63 +74,41 @@ jobs:
run: |
make e2e-compose-standalone
bin-image:
runs-on: ubuntu-22.04
bin-image-prepare:
runs-on: ubuntu-24.04
outputs:
digest: ${{ fromJSON(steps.bake.outputs.metadata).image-cross['containerimage.digest'] }}
repo-slug: ${{ env.REPO_SLUG }}
steps:
-
name: Free disk space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
-
name: Checkout
uses: actions/checkout@v4
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
# FIXME: can't use env object in reusable workflow inputs: https://github.com/orgs/community/discussions/26671
- run: echo "Exposing env vars for reusable workflow"
bin-image:
uses: docker/github-builder/.github/workflows/bake.yml@v1
needs:
- bin-image-prepare
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) with GitHub OIDC Token
with:
runner: amd64
target: image-cross
cache: true
cache-scope: bin-image
output: image
push: ${{ github.event_name != 'pull_request' }}
sbom: true
set-meta-labels: true
meta-images: |
${{ needs.bin-image-prepare.outputs.repo-slug }}
meta-tags: |
type=ref,event=tag
type=edge
meta-bake-target: meta-helper
secrets:
registry-auths: |
- registry: docker.io
username: ${{ secrets.DOCKERPUBLICBOT_USERNAME }}
password: ${{ secrets.DOCKERPUBLICBOT_WRITE_PAT }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REPO_SLUG }}
tags: |
type=ref,event=tag
type=edge
bake-target: meta-helper
-
name: Build and push image
uses: docker/bake-action@v6
id: bake
with:
source: .
files: |
./docker-bake.hcl
${{ steps.meta.outputs.bake-file }}
targets: image-cross
push: ${{ github.event_name != 'pull_request' }}
sbom: true
provenance: mode=max
set: |
*.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max
desktop-edge-test:
runs-on: ubuntu-latest
@@ -158,6 +136,6 @@ jobs:
workflow_id: 'compose-edge-integration.yml',
ref: 'main',
inputs: {
"image-tag": "${{ needs.bin-image.outputs.digest }}"
"image-tag": "${{ env.REPO_SLUG }}:edge"
}
})

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ bin/
coverage.out
covdatafiles/
.DS_Store
pkg/e2e/*.tar

1
.go-version Normal file
View File

@@ -0,0 +1 @@
1.25.7

View File

@@ -8,6 +8,7 @@ linters:
- depguard
- errcheck
- errorlint
- forbidigo
- gocritic
- gocyclo
- gomodguard
@@ -30,12 +31,23 @@ 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
desc: use stdlib slices package
- pkg: gopkg.in/yaml.v2
desc: compose-go uses yaml.v3
forbidigo:
analyze-types: true
forbid:
- pattern: 'context\.Background'
pkg: '^context$'
msg: "in tests, use t.Context() instead of context.Background()"
- pattern: 'context\.TODO'
pkg: '^context$'
msg: "in tests, use t.Context() instead of context.TODO()"
gocritic:
disabled-checks:
- paramTypeCombine
@@ -72,16 +84,27 @@ linters:
- third_party$
- builtin$
- examples$
rules:
- path-except: '_test\.go'
linters:
- forbidigo
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gci
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
settings:
gci:
sections:
- standard
- default
- localmodule
custom-order: true # make the section order the same as the order of "sections".

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,9 +15,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
ARG GO_VERSION=1.23.10
ARG XX_VERSION=1.6.1
ARG GOLANGCI_LINT_VERSION=v2.0.2
ARG GO_VERSION=1.25.7
ARG XX_VERSION=1.9.0
ARG GOLANGCI_LINT_VERSION=v2.8.0
ARG ADDLICENSE_VERSION=v1.0.0
ARG BUILD_TAGS="e2e"
@@ -28,12 +28,12 @@ ARG LICENSE_FILES=".*\(Dockerfile\|Makefile\|\.go\|\.hcl\|\.sh\)"
FROM --platform=${BUILDPLATFORM} tonistiigi/xx:${XX_VERSION} AS xx
# osxcross contains the MacOSX cross toolchain for xx
FROM crazymax/osxcross:11.3-alpine AS osxcross
FROM crazymax/osxcross:15.5-alpine AS osxcross
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
FROM ghcr.io/google/addlicense:${ADDLICENSE_VERSION} AS addlicense
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine AS base
FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine3.22 AS base
COPY --from=xx / /
RUN apk add --no-cache \
clang \
@@ -83,7 +83,7 @@ RUN --mount=type=bind,target=. \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=bind,from=osxcross,src=/osxsdk,target=/xx-sdk \
xx-go --wrap && \
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; fi && \
if [ "$(xx-info os)" == "darwin" ]; then export CGO_ENABLED=1; export BUILD_TAGS=fsnotify,$BUILD_TAGS; fi && \
make build GO_BUILDTAGS="$BUILD_TAGS" DESTDIR=/out && \
xx-verify --static /out/docker-compose

View File

@@ -1,124 +0,0 @@
# Docker maintainers file
#
# This file describes who runs the docker/compose project and how.
# This is a living document - if you see something out of date or missing, speak up!
#
# It is structured to be consumable by both humans and programs.
# To extract its contents programmatically, use any TOML-compliant
# parser.
#
# This file is compiled into the MAINTAINERS file in docker/opensource.
#
[Org]
[Org."Core maintainers"]
# The Core maintainers are the ghostbusters of the project: when there's a problem others
# can't solve, they show up and fix it with bizarre devices and weaponry.
# They have final say on technical implementation and coding style.
# They are ultimately responsible for quality in all its forms: usability polish,
# bugfixes, performance, stability, etc. When ownership can cleanly be passed to
# a subsystem, they are responsible for doing so and holding the
# subsystem maintainers accountable. If ownership is unclear, they are the de facto owners.
people = [
"glours",
"jhrotko",
"milas",
"ndeloof",
"nicksieger",
"StefanScherer",
"ulyssessouza"
]
[Org."Regular maintainers"]
# The Regular maintainers are people who aren't Core maintainers but are around
# to help reviewing and fixing bugs, just on a less regular basis than previously.
# Most of them were previously Core maintainers of Compose.
people = [
"aiordache",
"chris-crone",
"gtardif",
"laurazard",
"maxcleme",
"rumpl",
"thaJeztah"
]
[people]
# A reference list of all people associated with the project.
# All other sections should refer to people by their canonical key
# in the people section.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.aiordache]
Name = "Anca Iordache"
Email = "anca.iordache@docker.com"
GitHub = "aiordache "
[people.chris-crone]
Name = "Christopher Crone"
Email = "christopher.crone@docker.com"
GitHub = "chris-crone"
[people.glours]
Name = "Guillaume Lours"
Email = "guillaume.lours@docker.com"
GitHub = "glours"
[people.gtardif]
Name = "Guillaume Tardif"
Email = "guillaume.tardif@docker.com"
GitHub = "gtardif"
[people.jhrotko]
Name = "Joana Hrotko"
Email = "joana.hrotko@docker.com"
Github = "jhrotko"
[people.laurazard]
Name = "Laura Brehm"
Email = "laura.brehm@docker.com"
GitHub = "laurazard"
[people.maxcleme]
Name = "Maxime Clement"
Email = "maxime.clement@docker.com"
GitHub = "maxcleme"
[people.milas]
Name = "Milas Bowman"
Email = "milas.bowman@docker.com"
GitHub = "milas"
[people.nicksieger]
Name = "Nick Sieger"
Email = "nick.sieger@docker.com"
GitHub = "nicksieger"
[people.ndeloof]
Name = "Nicolas Deloof"
Email = "nicolas.deloof@docker.com"
GitHub = "ndeloof"
[people.rumpl]
Name = "Djordje Lukic"
Email = "djordje.lukic@docker.com"
GitHub = "rumpl"
[people.thaJeztah]
Name = "Sebastiaan van Stijn"
Email = "sebastiaan.vanstijn@docker.com"
GitHub = "thaJeztah "
[people.StefanScherer]
Name = "Stefan Scherer"
Email = "stefan.scherer@docker.com"
GitHub = "StefanScherer"
[people.ulyssessouza]
Name = "Ulysses Souza"
Email = "<ulysses.souza@docker.com"
Github = "ulyssessouza"

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
PKG := github.com/docker/compose/v2
PKG := github.com/docker/compose/v5
VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
GO_LDFLAGS ?= -w -X ${PKG}/internal.Version=${VERSION}
@@ -62,11 +62,11 @@ build:
.PHONY: binary
binary:
$(BUILDX_CMD) bake binary
BUILD_TAGS="$(GO_BUILDTAGS)" $(BUILDX_CMD) bake binary
.PHONY: binary-with-coverage
binary-with-coverage:
$(BUILDX_CMD) bake binary-with-coverage
BUILD_TAGS="$(GO_BUILDTAGS)" $(BUILDX_CMD) bake binary-with-coverage
.PHONY: install
install: binary

View File

@@ -1,17 +1,18 @@
# Table of Contents
- [Docker Compose v2](#docker-compose-v2)
- [Docker Compose](#docker-compose)
- [Where to get Docker Compose](#where-to-get-docker-compose)
+ [Windows and macOS](#windows-and-macos)
+ [Linux](#linux)
- [Quick Start](#quick-start)
- [Contributing](#contributing)
- [Legacy](#legacy)
# Docker Compose v2
# Docker Compose
[![GitHub release](https://img.shields.io/github/v/release/docker/compose.svg?style=flat-square)](https://github.com/docker/compose/releases/latest)
[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?style=flat-square&logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/compose/v2)
[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?style=flat-square&logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/compose/v5)
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/compose/ci.yml?label=ci&logo=github&style=flat-square)](https://github.com/docker/compose/actions?query=workflow%3Aci)
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/compose/v2?style=flat-square)](https://goreportcard.com/report/github.com/docker/compose/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/compose/v5?style=flat-square)](https://goreportcard.com/report/github.com/docker/compose/v5)
[![Codecov](https://codecov.io/gh/docker/compose/branch/main/graph/badge.svg?token=HP3K4Y4ctu)](https://codecov.io/gh/docker/compose)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/docker/compose/badge)](https://api.securityscorecards.dev/projects/github.com/docker/compose)
![Docker Compose](logo.png?raw=true "Docker Compose Logo")
@@ -23,6 +24,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 adopt 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

@@ -26,14 +26,15 @@ import (
dockercli "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/docker/compose/v2/internal/tracing"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
commands "github.com/docker/compose/v5/cmd/compose"
"github.com/docker/compose/v5/internal/tracing"
)
// Setup should be called as part of the command's PersistentPreRunE
@@ -55,8 +56,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

@@ -20,9 +20,10 @@ import (
"reflect"
"testing"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
commands "github.com/docker/compose/v5/cmd/compose"
)
func TestGetFlags(t *testing.T) {

View File

@@ -21,7 +21,7 @@ import (
"os"
"strings"
"github.com/docker/compose/v2/cmd/compose"
"github.com/docker/compose/v5/cmd/compose"
)
func getCompletionCommands() []string {

View File

@@ -1,5 +1,4 @@
/*
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.
@@ -16,12 +15,11 @@ package compose
import (
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
// alphaCommand groups all experimental subcommands
func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
cmd := &cobra.Command{
Short: "Experimental commands",
Use: "alpha [COMMAND]",
@@ -31,9 +29,9 @@ func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
},
}
cmd.AddCommand(
vizCommand(p, dockerCli, backend),
publishCommand(p, dockerCli, backend),
generateCommand(p, backend),
vizCommand(p, dockerCli, backendOptions),
publishCommand(p, dockerCli, backendOptions),
generateCommand(p, dockerCli, backendOptions),
)
return cmd
}

View File

@@ -20,8 +20,10 @@ import (
"context"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type attachOpts struct {
@@ -35,7 +37,7 @@ type attachOpts struct {
proxy bool
}
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func attachCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := attachOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
@@ -50,7 +52,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runAttach(ctx, dockerCli, backend, opts)
return runAttach(ctx, dockerCli, backendOptions, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -63,7 +65,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return runCmd
}
func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error {
func runAttach(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts attachOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
@@ -76,5 +78,9 @@ func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service,
NoStdin: opts.noStdin,
Proxy: opts.proxy,
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Attach(ctx, projectName, attachOpts)
}

View File

@@ -28,8 +28,9 @@ import (
"github.com/docker/go-units"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/bridge"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/bridge"
"github.com/docker/compose/v5/pkg/compose"
)
func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
@@ -62,7 +63,12 @@ func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
}
func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
project, _, err := p.ToProject(ctx, dockerCli, nil)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := p.ToProject(ctx, dockerCli, backend, nil)
if err != nil {
return err
}

View File

@@ -26,10 +26,11 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
cliopts "github.com/docker/cli/opts"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type buildOptions struct {
@@ -45,7 +46,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) {
@@ -65,8 +67,8 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
builderName = os.Getenv("BUILDX_BUILDER")
}
uiMode := ui.Mode
if uiMode == ui.ModeJSON {
uiMode := display.Mode
if uiMode == display.ModeJSON {
uiMode = "rawjson"
}
@@ -84,11 +86,12 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
Check: opts.check,
SSHs: SSHKeys,
Builder: builderName,
SBOM: opts.sbom,
Provenance: opts.provenance,
}, nil
}
func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := buildOptions{
ProjectOptions: p,
}
@@ -97,7 +100,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
Short: "Build or rebuild services",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet {
ui.Mode = ui.ModeQuiet
display.Mode = display.ModeQuiet
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
@@ -113,18 +116,20 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
if cmd.Flags().Changed("progress") && opts.ssh == "" {
fmt.Fprint(os.Stderr, "--progress is a global compose flag, better use `docker compose --progress xx build ...\n")
}
return runBuild(ctx, dockerCli, backend, opts, args)
return runBuild(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
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
@@ -144,9 +149,17 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts buildOptions, services []string) error {
if opts.print {
backendOptions.Add(compose.WithEventProcessor(display.Quiet()))
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
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)
project, _, err := opts.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
@@ -156,10 +169,10 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o
}
apiBuildOptions, err := opts.toAPIBuildOptions(services)
apiBuildOptions.Provenance = true
if err != nil {
return err
}
apiBuildOptions.Attestations = true
return backend.Build(ctx, project, apiBuildOptions)
}

View File

@@ -21,8 +21,10 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type commitOptions struct {
@@ -39,7 +41,7 @@ type commitOptions struct {
index int
}
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func commitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
options := commitOptions{
ProjectOptions: p,
}
@@ -56,7 +58,7 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runCommit(ctx, dockerCli, backend, options)
return runCommit(ctx, dockerCli, backendOptions, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -73,13 +75,17 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return cmd
}
func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service, options commitOptions) error {
func runCommit(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, options commitOptions) error {
projectName, err := options.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
commitOptions := api.CommitOptions{
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Commit(ctx, projectName, api.CommitOptions{
Service: options.service,
Reference: options.reference,
Pause: options.pause,
@@ -87,7 +93,5 @@ func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service,
Author: options.author,
Changes: options.changes,
Index: options.index,
}
return backend.Commit(ctx, projectName, commitOptions)
})
}

View File

@@ -21,8 +21,10 @@ import (
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
// validArgsFn defines a completion func to be returned to fetch completion options
@@ -37,7 +39,12 @@ func noCompletion() validArgsFn {
func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p.Offline = true
project, _, err := p.ToProject(cmd.Context(), dockerCli, nil)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
@@ -52,8 +59,13 @@ func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn
}
}
func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
func completeProjectNames(dockerCli command.Cli, backendOptions *BackendOptions) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
list, err := backend.List(cmd.Context(), api.ListOptions{
All: true,
})
@@ -73,7 +85,12 @@ func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []s
func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p.Offline = true
project, _, err := p.ToProject(cmd.Context(), dockerCli, nil)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

View File

@@ -32,25 +32,26 @@ import (
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/loader"
composepaths "github.com/compose-spec/compose-go/v2/paths"
"github.com/compose-spec/compose-go/v2/types"
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"
"github.com/docker/compose/v2/internal/desktop"
"github.com/docker/compose/v2/internal/experimental"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/remote"
"github.com/docker/compose/v2/pkg/utils"
"github.com/morikuni/aec"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/remote"
"github.com/docker/compose/v5/pkg/utils"
)
const (
@@ -59,7 +60,7 @@ const (
// ComposeProjectName define the project name to be used, instead of guessing from parent directory
ComposeProjectName = "COMPOSE_PROJECT_NAME"
// ComposeCompatibility try to mimic compose v1 as much as possible
ComposeCompatibility = "COMPOSE_COMPATIBILITY"
ComposeCompatibility = api.ComposeCompatibility
// ComposeRemoveOrphans remove "orphaned" containers, i.e. containers tagged for current project but not declared as service
ComposeRemoveOrphans = "COMPOSE_REMOVE_ORPHANS"
// ComposeIgnoreOrphans ignore "orphaned" containers
@@ -85,18 +86,16 @@ func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(ke
return nil
}
var stdioToStdout bool
func init() {
// compose evaluates env file values for interpolation
// `raw` format allows to load env_file with the same parser used by docker run --env-file
dotenv.RegisterFormat("raw", rawEnv)
}
type Backend interface {
api.Service
SetDesktopClient(cli *desktop.Client)
SetExperiments(experiments *experimental.State)
if v, ok := os.LookupEnv("COMPOSE_STATUS_STDOUT"); ok {
stdioToStdout, _ = strconv.ParseBool(v)
}
}
// Command defines a compose CLI command as a func with args
@@ -125,7 +124,7 @@ func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
StatusCode: 130,
}
}
if ui.Mode == ui.ModeJSON {
if display.Mode == display.ModeJSON {
err = makeJSONError(err)
}
return err
@@ -140,16 +139,17 @@ func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
}
type ProjectOptions struct {
ProjectName string
Profiles []string
ConfigPaths []string
WorkDir string
ProjectDir string
EnvFiles []string
Compatibility bool
Progress string
Offline bool
All bool
ProjectName string
Profiles []string
ConfigPaths []string
WorkDir string
ProjectDir string
EnvFiles []string
Compatibility bool
Progress string
Offline bool
All bool
insecureRegistries []string
}
// ProjectFunc does stuff within a types.Project
@@ -167,13 +167,13 @@ func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func
// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
return Adapt(func(ctx context.Context, args []string) error {
options := []cli.ProjectOptionsFn{
cli.WithResolvedPaths(true),
cli.WithoutEnvironmentResolution,
return Adapt(func(ctx context.Context, services []string) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, metrics, err := o.ToProject(ctx, dockerCli, args, options...)
project, metrics, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
@@ -185,7 +185,7 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF
return err
}
return fn(ctx, project, args)
return fn(ctx, project, services)
})
}
@@ -225,6 +225,8 @@ func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
f.StringArrayVar(&o.Profiles, "profile", []string{}, "Specify a profile to enable")
f.StringVarP(&o.ProjectName, "project-name", "p", "", "Project name")
f.StringArrayVarP(&o.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
f.StringArrayVar(&o.insecureRegistries, "insecure-registry", []string{}, "Use insecure registry to pull Compose OCI artifacts. Doesn't apply to images")
_ = f.MarkHidden("insecure-registry")
f.StringArrayVar(&o.EnvFiles, "env-file", defaultStringArrayVar(ComposeEnvFiles), "Specify an alternate environment file")
f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
@@ -245,7 +247,12 @@ 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)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, "", err
}
p, _, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
if err != nil {
envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" {
@@ -269,7 +276,12 @@ func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cl
return envProjectName, nil
}
project, _, err := o.ToProject(ctx, dockerCli, nil)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return "", err
}
project, _, err := o.ToProject(ctx, dockerCli, backend, nil)
if err != nil {
return "", err
}
@@ -294,19 +306,14 @@ func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, ser
return options.LoadModel(ctx)
}
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { //nolint:gocyclo
// ToProject loads a Compose project using the LoadProject API.
// Accepts optional cli.ProjectOptionsFn to control loader behavior.
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) {
var metrics tracing.Metrics
remotes := o.remoteLoaders(dockerCli)
for _, r := range remotes {
po = append(po, cli.WithResourceLoader(r))
}
options, err := o.toProjectOptions(po...)
if err != nil {
return nil, metrics, err
}
options.WithListeners(func(event string, metadata map[string]any) {
// Setup metrics listener to collect project data
metricsListener := func(event string, metadata map[string]any) {
switch event {
case "extends":
metrics.CountExtends++
@@ -327,50 +334,31 @@ func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, s
}
}
}
})
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
api.Separator = "_"
}
project, err := options.LoadProject(ctx)
loadOpts := api.ProjectLoadOptions{
ProjectName: o.ProjectName,
ConfigPaths: o.ConfigPaths,
WorkingDir: o.ProjectDir,
EnvFiles: o.EnvFiles,
Profiles: o.Profiles,
Services: services,
Offline: o.Offline,
All: o.All,
Compatibility: o.Compatibility,
ProjectOptionsFns: po,
LoadListeners: []api.LoadListener{metricsListener},
OCI: api.OCIOptions{
InsecureRegistries: o.insecureRegistries,
},
}
project, err := backend.LoadProject(ctx, loadOpts)
if err != nil {
return nil, metrics, err
}
if project.Name == "" {
return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name")
}
project, err = project.WithServicesEnabled(services...)
if err != nil {
return nil, metrics, err
}
for name, s := range project.Services {
s.CustomLabels = map[string]string{
api.ProjectLabel: project.Name,
api.ServiceLabel: name,
api.VersionLabel: api.ComposeVersion,
api.WorkingDirLabel: project.WorkingDir,
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
api.OneoffLabel: "False", // default, will be overridden by `run` command
}
if len(o.EnvFiles) != 0 {
s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(o.EnvFiles, ",")
}
project.Services[name] = s
}
project, err = project.WithSelectedServices(services)
if err != nil {
return nil, tracing.Metrics{}, err
}
if !o.All {
project = project.WithoutUnnecessaryResources()
}
return project, metrics, err
return project, metrics, nil
}
func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader {
@@ -378,37 +366,43 @@ func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceL
return nil
}
git := remote.NewGitRemoteLoader(dockerCli, o.Offline)
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline, api.OCIOptions{})
return []loader.ResourceLoader{git, oci}
}
func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.ProjectOptions, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
opts := []cli.ProjectOptionsFn{
cli.WithWorkingDirectory(o.ProjectDir),
// First apply os.Environment, always win
cli.WithOsEnv,
}
return cli.NewProjectOptions(o.ConfigPaths,
append(po,
cli.WithWorkingDirectory(o.ProjectDir),
// First apply os.Environment, always win
cli.WithOsEnv,
// set PWD as this variable is not consistently supported on Windows
cli.WithEnv([]string{"PWD=" + pwd}),
// Load PWD/.env if present and no explicit --env-file has been set
cli.WithEnvFiles(o.EnvFiles...),
// read dot env file to populate project environment
cli.WithDotEnv,
// get compose file path set by COMPOSE_FILE
cli.WithConfigFileEnv,
// if none was selected, get default compose.yaml file from current dir or parent folder
cli.WithDefaultConfigPath,
// .. and then, a project directory != PWD maybe has been set so let's load .env file
cli.WithEnvFiles(o.EnvFiles...),
cli.WithDotEnv,
// eventually COMPOSE_PROFILES should have been set
cli.WithDefaultProfiles(o.Profiles...),
cli.WithName(o.ProjectName))...)
if _, present := os.LookupEnv("PWD"); !present {
if pwd, err := os.Getwd(); err != nil {
return nil, err
} else {
opts = append(opts, cli.WithEnv([]string{"PWD=" + pwd}))
}
}
opts = append(opts,
// Load PWD/.env if present and no explicit --env-file has been set
cli.WithEnvFiles(o.EnvFiles...),
// read dot env file to populate project environment
cli.WithDotEnv,
// get compose file path set by COMPOSE_FILE
cli.WithConfigFileEnv,
// if none was selected, get default compose.yaml file from current dir or parent folder
cli.WithDefaultConfigPath,
// .. and then, a project directory != PWD maybe has been set so let's load .env file
cli.WithEnvFiles(o.EnvFiles...), //nolint:gocritic // intentionally applying cli.WithEnvFiles twice.
cli.WithDotEnv, //nolint:gocritic // intentionally applying cli.WithDotEnv twice.
// eventually COMPOSE_PROFILES should have been set
cli.WithDefaultProfiles(o.Profiles...),
cli.WithName(o.ProjectName),
)
return cli.NewProjectOptions(o.ConfigPaths, append(po, opts...)...)
}
// PluginName is the name of the plugin
@@ -416,11 +410,19 @@ 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
}
type BackendOptions struct {
Options []compose.Option
}
func (o *BackendOptions) Add(option compose.Option) {
o.Options = append(o.Options, option)
}
// RootCommand returns the compose command with its child commands
func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //nolint:gocyclo
func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { //nolint:gocyclo
// filter out useless commandConn.CloseWrite warning message that can occur
// when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
@@ -431,7 +433,6 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
"commandConn.CloseRead:",
))
experiments := experimental.NewState()
opts := ProjectOptions{}
var (
ansi string
@@ -461,9 +462,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
}
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
parent := cmd.Root()
if parent != nil {
parentPrerun := parent.PersistentPreRunE
if parentPrerun != nil {
@@ -478,7 +477,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
logrus.SetLevel(logrus.TraceLevel)
}
err := setEnvWithDotEnv(opts)
err := setEnvWithDotEnv(opts, dockerCli)
if err != nil {
return err
}
@@ -495,40 +494,54 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
formatter.SetANSIMode(dockerCli, ansi)
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
ui.NoColor()
display.NoColor()
formatter.SetANSIMode(dockerCli, formatter.Never)
}
switch ansi {
case "never":
ui.Mode = ui.ModePlain
display.Mode = display.ModePlain
case "always":
ui.Mode = ui.ModeTTY
display.Mode = display.ModeTTY
}
detached, _ := cmd.Flags().GetBool("detach")
var ep api.EventProcessor
switch opts.Progress {
case "", ui.ModeAuto:
if ansi == "never" {
ui.Mode = ui.ModePlain
case "", display.ModeAuto:
switch {
case ansi == "never":
display.Mode = display.ModePlain
ep = display.Plain(dockerCli.Err())
case dockerCli.Out().IsTerminal():
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli), detached)
default:
ep = display.Plain(dockerCli.Err())
}
case ui.ModeTTY:
case display.ModeTTY:
if ansi == "never" {
return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
}
ui.Mode = ui.ModeTTY
case ui.ModePlain:
display.Mode = display.ModeTTY
ep = display.Full(dockerCli.Err(), stdinfo(dockerCli), detached)
case display.ModePlain:
if ansi == "always" {
return fmt.Errorf("can't use --progress plain while ANSI support is forced")
}
ui.Mode = ui.ModePlain
case ui.ModeQuiet, "none":
ui.Mode = ui.ModeQuiet
case ui.ModeJSON:
ui.Mode = ui.ModeJSON
display.Mode = display.ModePlain
ep = display.Plain(dockerCli.Err())
case display.ModeQuiet, "none":
display.Mode = display.ModeQuiet
ep = display.Quiet()
case display.ModeJSON:
display.Mode = display.ModeJSON
logrus.SetFormatter(&logrus.JSONFormatter{})
ep = display.JSON(dockerCli.Err())
default:
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
}
backendOptions.Add(compose.WithEventProcessor(ep))
// (4) options validation / normalization
if opts.WorkDir != "" {
@@ -539,12 +552,15 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF))
}
for i, file := range opts.EnvFiles {
file = composepaths.ExpandUser(file)
if !filepath.IsAbs(file) {
file, err := filepath.Abs(file)
if err != nil {
return err
}
opts.EnvFiles[i] = file
} else {
opts.EnvFiles[i] = file
}
}
@@ -565,85 +581,61 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
}
if parallel > 0 {
logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
backend.MaxConcurrency(parallel)
backendOptions.Add(compose.WithMaxConcurrency(parallel))
}
// dry run detection
ctx, err = backend.DryRunMode(ctx, dryRun)
if err != nil {
return err
if dryRun {
backendOptions.Add(compose.WithDryRun)
}
cmd.SetContext(ctx)
// (6) Desktop integration
var desktopCli *desktop.Client
if !dryRun {
if desktopCli, err = desktop.NewFromDockerClient(ctx, dockerCli); desktopCli != nil {
logrus.Debugf("Enabled Docker Desktop integration (experimental) @ %s", desktopCli.Endpoint())
backend.SetDesktopClient(desktopCli)
} else if err != nil {
// not fatal, Compose will still work but behave as though
// it's not running as part of Docker Desktop
logrus.Debugf("failed to enable Docker Desktop integration: %v", err)
} else {
logrus.Trace("Docker Desktop integration not enabled")
}
}
// (7) experimental features
if err := experiments.Load(ctx, desktopCli); err != nil {
logrus.Debugf("Failed to query feature flags from Desktop: %v", err)
}
backend.SetExperiments(experiments)
return nil
},
}
c.AddCommand(
upCommand(&opts, dockerCli, backend),
downCommand(&opts, dockerCli, backend),
startCommand(&opts, dockerCli, backend),
restartCommand(&opts, dockerCli, backend),
stopCommand(&opts, dockerCli, backend),
psCommand(&opts, dockerCli, backend),
listCommand(dockerCli, backend),
logsCommand(&opts, dockerCli, backend),
upCommand(&opts, dockerCli, backendOptions),
downCommand(&opts, dockerCli, backendOptions),
startCommand(&opts, dockerCli, backendOptions),
restartCommand(&opts, dockerCli, backendOptions),
stopCommand(&opts, dockerCli, backendOptions),
psCommand(&opts, dockerCli, backendOptions),
listCommand(dockerCli, backendOptions),
logsCommand(&opts, dockerCli, backendOptions),
configCommand(&opts, dockerCli),
killCommand(&opts, dockerCli, backend),
runCommand(&opts, dockerCli, backend),
removeCommand(&opts, dockerCli, backend),
execCommand(&opts, dockerCli, backend),
attachCommand(&opts, dockerCli, backend),
exportCommand(&opts, dockerCli, backend),
commitCommand(&opts, dockerCli, backend),
pauseCommand(&opts, dockerCli, backend),
unpauseCommand(&opts, dockerCli, backend),
topCommand(&opts, dockerCli, backend),
eventsCommand(&opts, dockerCli, backend),
portCommand(&opts, dockerCli, backend),
imagesCommand(&opts, dockerCli, backend),
killCommand(&opts, dockerCli, backendOptions),
runCommand(&opts, dockerCli, backendOptions),
removeCommand(&opts, dockerCli, backendOptions),
execCommand(&opts, dockerCli, backendOptions),
attachCommand(&opts, dockerCli, backendOptions),
exportCommand(&opts, dockerCli, backendOptions),
commitCommand(&opts, dockerCli, backendOptions),
pauseCommand(&opts, dockerCli, backendOptions),
unpauseCommand(&opts, dockerCli, backendOptions),
topCommand(&opts, dockerCli, backendOptions),
eventsCommand(&opts, dockerCli, backendOptions),
portCommand(&opts, dockerCli, backendOptions),
imagesCommand(&opts, dockerCli, backendOptions),
versionCommand(dockerCli),
buildCommand(&opts, dockerCli, backend),
pushCommand(&opts, dockerCli, backend),
pullCommand(&opts, dockerCli, backend),
createCommand(&opts, dockerCli, backend),
copyCommand(&opts, dockerCli, backend),
waitCommand(&opts, dockerCli, backend),
scaleCommand(&opts, dockerCli, backend),
buildCommand(&opts, dockerCli, backendOptions),
pushCommand(&opts, dockerCli, backendOptions),
pullCommand(&opts, dockerCli, backendOptions),
createCommand(&opts, dockerCli, backendOptions),
copyCommand(&opts, dockerCli, backendOptions),
waitCommand(&opts, dockerCli, backendOptions),
scaleCommand(&opts, dockerCli, backendOptions),
statsCommand(&opts, dockerCli),
watchCommand(&opts, dockerCli, backend),
publishCommand(&opts, dockerCli, backend),
alphaCommand(&opts, dockerCli, backend),
watchCommand(&opts, dockerCli, backendOptions),
publishCommand(&opts, dockerCli, backendOptions),
alphaCommand(&opts, dockerCli, backendOptions),
bridgeCommand(&opts, dockerCli),
volumesCommand(&opts, dockerCli, backend),
volumesCommand(&opts, dockerCli, backendOptions),
)
c.Flags().SetInterspersed(false)
opts.addProjectFlags(c.Flags())
c.RegisterFlagCompletionFunc( //nolint:errcheck
"project-name",
completeProjectNames(backend),
completeProjectNames(dockerCli, backendOptions),
)
c.RegisterFlagCompletionFunc( //nolint:errcheck
"project-directory",
@@ -661,6 +653,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`)
@@ -674,7 +670,28 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
return c
}
func setEnvWithDotEnv(opts ProjectOptions) error {
func stdinfo(dockerCli command.Cli) io.Writer {
if stdioToStdout {
return dockerCli.Out()
}
return dockerCli.Err()
}
func setEnvWithDotEnv(opts ProjectOptions, dockerCli command.Cli) error {
// Check if we're using a remote config (OCI or Git)
// If so, skip env loading as remote loaders haven't been initialized yet
// and trying to process the path would fail
remoteLoaders := opts.remoteLoaders(dockerCli)
for _, path := range opts.ConfigPaths {
for _, loader := range remoteLoaders {
if loader.Accept(path) {
// Remote config - skip env loading for now
// It will be loaded later when the project is fully initialized
return nil
}
}
}
options, err := cli.NewProjectOptions(opts.ConfigPaths,
cli.WithWorkingDirectory(opts.ProjectDir),
cli.WithOsEnv,
@@ -682,38 +699,26 @@ func setEnvWithDotEnv(opts ProjectOptions) error {
cli.WithDotEnv,
)
if err != nil {
return nil
return err
}
envFromFile, err := dotenv.GetEnvFromFile(composegoutils.GetAsEqualsMap(os.Environ()), options.EnvFiles)
if err != nil {
return nil
return err
}
for k, v := range envFromFile {
if _, ok := os.LookupEnv(k); !ok {
if err = os.Setenv(k, v); err != nil {
return nil
if _, ok := os.LookupEnv(k); !ok && strings.HasPrefix(k, "COMPOSE_") {
if err := os.Setenv(k, v); err != nil {
return err
}
}
}
return err
return nil
}
var printerModes = []string{
ui.ModeAuto,
ui.ModeTTY,
ui.ModePlain,
ui.ModeJSON,
ui.ModeQuiet,
}
func SetUnchangedOption(name string, experimentalFlag bool) bool {
var value bool
// If the var is defined we use that value first
if envVar, ok := os.LookupEnv(name); ok {
value = utils.StringToBool(envVar)
} else {
// if not, we try to get it from experimental feature flag
value = experimentalFlag
}
return value
display.ModeAuto,
display.ModeTTY,
display.ModePlain,
display.ModeJSON,
display.ModeQuiet,
}

View File

@@ -0,0 +1,76 @@
/*
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 (
"testing"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestSetEnvWithDotEnv_WithOCIArtifact(t *testing.T) {
// Test that setEnvWithDotEnv doesn't fail when using OCI artifacts
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
opts := ProjectOptions{
ConfigPaths: []string{"oci://docker.io/dockersamples/welcome-to-docker"},
ProjectDir: "",
EnvFiles: []string{},
}
err := setEnvWithDotEnv(opts, cli)
assert.NilError(t, err, "setEnvWithDotEnv should not fail with OCI artifact path")
}
func TestSetEnvWithDotEnv_WithGitRemote(t *testing.T) {
// Test that setEnvWithDotEnv doesn't fail when using Git remotes
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
opts := ProjectOptions{
ConfigPaths: []string{"https://github.com/docker/compose.git"},
ProjectDir: "",
EnvFiles: []string{},
}
err := setEnvWithDotEnv(opts, cli)
assert.NilError(t, err, "setEnvWithDotEnv should not fail with Git remote path")
}
func TestSetEnvWithDotEnv_WithLocalPath(t *testing.T) {
// Test that setEnvWithDotEnv still works with local paths
// This will fail if the file doesn't exist, but it should not panic
// or produce invalid paths
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
opts := ProjectOptions{
ConfigPaths: []string{"compose.yaml"},
ProjectDir: "",
EnvFiles: []string{},
}
// This may error if files don't exist, but should not panic
_ = setEnvWithDotEnv(opts, cli)
}

View File

@@ -30,12 +30,12 @@ import (
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type configOptions struct {
@@ -51,6 +51,7 @@ type configOptions struct {
services bool
volumes bool
networks bool
models bool
profiles bool
images bool
hash string
@@ -60,19 +61,19 @@ type configOptions struct {
lockImageDigests bool
}
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
po = append(po, o.ToProjectOptions()...)
project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, services, po...)
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string) (*types.Project, error) {
project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, backend, services, o.toProjectOptionsFns()...)
return project, err
}
func (o *configOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
po = append(po, o.ToProjectOptions()...)
po = append(po, o.toProjectOptionsFns()...)
return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...)
}
func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn {
return []cli.ProjectOptionsFn{
// toProjectOptionsFns converts config options to cli.ProjectOptionsFn
func (o *configOptions) toProjectOptionsFns() []cli.ProjectOptionsFn {
fns := []cli.ProjectOptionsFn{
cli.WithInterpolation(!o.noInterpolate),
cli.WithResolvedPaths(!o.noResolvePath),
cli.WithNormalization(!o.noNormalize),
@@ -80,6 +81,10 @@ func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn {
cli.WithDefaultProfiles(o.Profiles...),
cli.WithDiscardEnvFile,
}
if o.noResolveEnv {
fns = append(fns, cli.WithoutEnvironmentResolution)
}
return fns
}
func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
@@ -115,6 +120,9 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
if opts.networks {
return runNetworks(ctx, dockerCli, opts)
}
if opts.models {
return runModels(ctx, dockerCli, opts)
}
if opts.hash != "" {
return runHash(ctx, dockerCli, opts)
}
@@ -152,6 +160,7 @@ 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.")
@@ -192,7 +201,12 @@ func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, s
}
func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
project, err := opts.ToProject(ctx, dockerCli, services)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return nil, err
}
project, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return nil, err
}
@@ -319,17 +333,16 @@ func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[s
func formatModel(model map[string]any, format string) (content []byte, err error) {
switch format {
case "json":
content, err = json.MarshalIndent(model, "", " ")
return json.MarshalIndent(model, "", " ")
case "yaml":
buf := bytes.NewBuffer([]byte{})
encoder := yaml.NewEncoder(buf)
encoder.SetIndent(2)
err = encoder.Encode(model)
content = buf.Bytes()
return buf.Bytes(), err
default:
return nil, fmt.Errorf("unsupported format %q", format)
}
return
}
func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
@@ -349,7 +362,12 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions)
return nil
}
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
@@ -362,7 +380,12 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions)
}
func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
@@ -373,7 +396,12 @@ func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions)
}
func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
@@ -383,12 +411,36 @@ func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions)
return nil
}
func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, 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 != "*" {
services = append(services, strings.Split(opts.hash, ",")...)
}
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
@@ -423,7 +475,13 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
set := map[string]struct{}{}
project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
@@ -444,7 +502,12 @@ func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions,
}
func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}
@@ -481,7 +544,12 @@ func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions
}
func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
project, err := opts.ToProject(ctx, dockerCli, services)
backend, err := compose.NewComposeService(dockerCli)
if err != nil {
return err
}
project, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}

View File

@@ -24,7 +24,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type copyOptions struct {
@@ -38,7 +39,7 @@ type copyOptions struct {
copyUIDGID bool
}
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func copyCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := copyOptions{
ProjectOptions: p,
}
@@ -59,7 +60,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.source = args[0]
opts.destination = args[1]
return runCopy(ctx, dockerCli, backend, opts)
return runCopy(ctx, dockerCli, backendOptions, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -73,12 +74,16 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return copyCmd
}
func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error {
func runCopy(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts copyOptions) error {
name, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Copy(ctx, name, api.CopyOptions{
Source: opts.source,
Destination: opts.destination,

View File

@@ -30,7 +30,8 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type createOptions struct {
@@ -51,7 +52,7 @@ type createOptions struct {
AssumeYes bool
}
func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func createCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: p,
@@ -70,7 +71,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return nil
}),
RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
return runCreate(ctx, dockerCli, backend, opts, buildOpts, project, services)
return runCreate(ctx, dockerCli, backendOptions, opts, buildOpts, project, services)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -95,7 +96,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return cmd
}
func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
func runCreate(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
if err := createOpts.Apply(project); err != nil {
return err
}
@@ -109,6 +110,14 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp
build = &bo
}
if createOpts.AssumeYes {
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Create(ctx, project, api.CreateOptions{
Build: build,
Services: services,
@@ -119,7 +128,6 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp
Inherit: !createOpts.noInherit,
Timeout: createOpts.GetTimeout(),
QuietPull: createOpts.quietPull,
AssumeYes: createOpts.AssumeYes,
})
}
@@ -190,12 +198,11 @@ func (opts createOptions) Apply(project *types.Project) error {
func applyScaleOpts(project *types.Project, opts []string) error {
for _, scale := range opts {
split := strings.Split(scale, "=")
if len(split) != 2 {
name, val, ok := strings.Cut(scale, "=")
if !ok || val == "" {
return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
}
name := split[0]
replicas, err := strconv.Atoi(split[1])
replicas, err := strconv.Atoi(val)
if err != nil {
return err
}

View File

@@ -1,174 +0,0 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"github.com/davecgh/go-spew/spew"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestRunCreate(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy(""),
deepEqual(defaultCreateOptions(true)),
)
createOpts := createOptions{}
buildOpts := buildOptions{
ProjectOptions: &ProjectOptions{},
}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}
func TestRunCreate_Build(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy("build"),
deepEqual(defaultCreateOptions(true)),
)
createOpts := createOptions{
Build: true,
}
buildOpts := buildOptions{
ProjectOptions: &ProjectOptions{},
}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}
func TestRunCreate_NoBuild(t *testing.T) {
ctrl, ctx := gomock.WithContext(context.Background(), t)
backend := mocks.NewMockService(ctrl)
backend.EXPECT().Create(
gomock.Eq(ctx),
pullPolicy(""),
deepEqual(defaultCreateOptions(false)),
)
createOpts := createOptions{
noBuild: true,
}
buildOpts := buildOptions{}
project := sampleProject()
err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
require.NoError(t, err)
}
func sampleProject() *types.Project {
return &types.Project{
Name: "test",
Services: types.Services{
"svc": {
Name: "svc",
Build: &types.BuildConfig{
Context: ".",
},
},
},
}
}
func defaultCreateOptions(includeBuild bool) api.CreateOptions {
var build *api.BuildOptions
if includeBuild {
bo := defaultBuildOptions()
build = &bo
}
return api.CreateOptions{
Build: build,
Services: nil,
RemoveOrphans: false,
IgnoreOrphans: false,
Recreate: "diverged",
RecreateDependencies: "diverged",
Inherit: true,
Timeout: nil,
QuietPull: false,
}
}
func defaultBuildOptions() api.BuildOptions {
return api.BuildOptions{
Args: make(types.MappingWithEquals),
Progress: "auto",
}
}
// deepEqual returns a nice diff on failure vs gomock.Eq when used
// on structs.
func deepEqual(x interface{}) gomock.Matcher {
return gomock.GotFormatterAdapter(
gomock.GotFormatterFunc(func(got interface{}) string {
return cmp.Diff(x, got)
}),
gomock.Eq(x),
)
}
func spewAdapter(m gomock.Matcher) gomock.Matcher {
return gomock.GotFormatterAdapter(
gomock.GotFormatterFunc(func(got interface{}) string {
return spew.Sdump(got)
}),
m,
)
}
type withPullPolicy struct {
policy string
}
func pullPolicy(policy string) gomock.Matcher {
return spewAdapter(withPullPolicy{policy: policy})
}
func (w withPullPolicy) Matches(x interface{}) bool {
proj, ok := x.(*types.Project)
if !ok || proj == nil || len(proj.Services) == 0 {
return false
}
for _, svc := range proj.Services {
if svc.PullPolicy != w.policy {
return false
}
}
return true
}
func (w withPullPolicy) String() string {
return fmt.Sprintf("has pull policy %q for all services", w.policy)
}

View File

@@ -23,12 +23,13 @@ import (
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/utils"
)
type downOptions struct {
@@ -40,7 +41,7 @@ type downOptions struct {
images string
}
func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := downOptions{
ProjectOptions: p,
}
@@ -57,9 +58,9 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runDown(ctx, dockerCli, backend, opts, args)
return runDown(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: noCompletion(),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := downCmd.Flags()
removeOrphans := utils.StringToBool(os.Getenv(ComposeRemoveOrphans))
@@ -77,7 +78,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return downCmd
}
func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error {
func runDown(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts downOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
@@ -88,6 +89,10 @@ func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, op
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Down(ctx, name, api.DownOptions{
RemoveOrphans: opts.removeOrphans,
Project: project,

View File

@@ -22,17 +22,20 @@ import (
"fmt"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type eventsOpts struct {
*composeOptions
json bool
json bool
since string
until string
}
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := eventsOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
@@ -42,26 +45,34 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
Use: "events [OPTIONS] [SERVICE...]",
Short: "Receive real time events from containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runEvents(ctx, dockerCli, backend, opts, args)
return runEvents(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
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
}
func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error {
func runEvents(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts eventsOpts, services []string) error {
name, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
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{}{
marshal, err := json.Marshal(map[string]any{
"time": event.Timestamp,
"type": "container",
"service": event.Service,

View File

@@ -25,10 +25,12 @@ import (
"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"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type execOpts struct {
@@ -47,7 +49,7 @@ type execOpts struct {
interactive bool
}
func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func execCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := execOpts{
composeOptions: &composeOptions{
ProjectOptions: p,
@@ -63,7 +65,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
err := runExec(ctx, dockerCli, backend, opts)
err := runExec(ctx, dockerCli, backendOptions, opts)
if err != nil {
logrus.Debugf("%v", err)
var cliError cli.StatusError
@@ -81,7 +83,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
runCmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process")
runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user")
runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
runCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY.")
runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command")
runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
@@ -90,10 +92,16 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
runCmd.Flags().SetInterspersed(false)
runCmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "no-TTY" { // legacy
name = "no-tty"
}
return pflag.NormalizedName(name)
})
return runCmd
}
func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error {
func runExec(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts execOpts) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
@@ -119,6 +127,10 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, op
Interactive: opts.interactive,
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
exitCode, err := backend.Exec(ctx, projectName, execOpts)
if exitCode != 0 {
errMsg := fmt.Sprintf("exit status %d", exitCode)

View File

@@ -22,7 +22,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type exportOptions struct {
@@ -33,7 +34,7 @@ type exportOptions struct {
index int
}
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func exportCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
options := exportOptions{
ProjectOptions: p,
}
@@ -46,7 +47,7 @@ func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runExport(ctx, dockerCli, backend, options)
return runExport(ctx, dockerCli, backendOptions, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -58,7 +59,7 @@ func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return cmd
}
func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error {
func runExport(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, options exportOptions) error {
projectName, err := options.toProjectName(ctx, dockerCli)
if err != nil {
return err
@@ -70,5 +71,9 @@ func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service,
Output: options.output,
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Export(ctx, projectName, exportOptions)
}

View File

@@ -21,8 +21,11 @@ import (
"fmt"
"os"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type generateOptions struct {
@@ -30,7 +33,7 @@ type generateOptions struct {
Format string
}
func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
func generateCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := generateOptions{
ProjectOptions: p,
}
@@ -42,7 +45,7 @@ func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runGenerate(ctx, backend, opts, args)
return runGenerate(ctx, dockerCli, backendOptions, opts, args)
}),
}
@@ -52,11 +55,16 @@ func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
return cmd
}
func runGenerate(ctx context.Context, backend api.Service, opts generateOptions, containers []string) error {
func runGenerate(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts generateOptions, containers []string) error {
_, _ = fmt.Fprintln(os.Stderr, "generate command is EXPERIMENTAL")
if len(containers) == 0 {
return fmt.Errorf("at least one container must be specified")
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, err := backend.Generate(ctx, api.GenerateOptions{
Containers: containers,
ProjectName: opts.ProjectName,
@@ -64,6 +72,7 @@ func runGenerate(ctx context.Context, backend api.Service, opts generateOptions,
if err != nil {
return err
}
var content []byte
switch opts.Format {
case "json":

View File

@@ -31,8 +31,9 @@ import (
"github.com/docker/go-units"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type imageOptions struct {
@@ -41,7 +42,7 @@ type imageOptions struct {
Format string
}
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := imageOptions{
ProjectOptions: p,
}
@@ -49,7 +50,7 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
Use: "images [OPTIONS] [SERVICE...]",
Short: "List images used by the created containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runImages(ctx, dockerCli, backend, opts, args)
return runImages(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -58,12 +59,16 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
return imgCmd
}
func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error {
func runImages(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts imageOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
images, err := backend.Images(ctx, projectName, api.ImagesOptions{
Services: services,
})
@@ -90,17 +95,19 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
if opts.Format == "json" {
type img struct {
ID string `json:"ID"`
ContainerName string `json:"ContainerName"`
Repository string `json:"Repository"`
Tag string `json:"Tag"`
Platform string `json:"Platform"`
Size int64 `json:"Size"`
LastTagTime time.Time `json:"LastTagTime"`
ID string `json:"ID"`
ContainerName string `json:"ContainerName"`
Repository string `json:"Repository"`
Tag string `json:"Tag"`
Platform string `json:"Platform"`
Size int64 `json:"Size"`
Created *time.Time `json:"Created,omitempty"`
LastTagTime time.Time `json:"LastTagTime,omitzero"`
}
// Convert map to slice
var imageList []img
for ctr, i := range images {
lastTagTime := i.LastTagTime
imageList = append(imageList, img{
ContainerName: ctr,
ID: i.ID,
@@ -108,7 +115,8 @@ 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,
Created: i.Created,
LastTagTime: lastTagTime,
})
}
json, err := formatter.ToJSON(imageList, "", "")
@@ -133,7 +141,10 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
if tag == "" {
tag = "<none>"
}
created := units.HumanDuration(time.Now().UTC().Sub(img.LastTagTime)) + " ago"
created := "N/A"
if img.Created != nil {
created = units.HumanDuration(time.Now().UTC().Sub(*img.Created)) + " ago"
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
container, repo, tag, platforms.Format(img.Platform), id, size, created)
}

View File

@@ -18,13 +18,16 @@ package compose
import (
"context"
"errors"
"fmt"
"os"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/utils"
)
type killOptions struct {
@@ -33,7 +36,7 @@ type killOptions struct {
signal string
}
func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func killCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := killOptions{
ProjectOptions: p,
}
@@ -41,7 +44,7 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
Use: "kill [OPTIONS] [SERVICE...]",
Short: "Force stop service containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runKill(ctx, dockerCli, backend, opts, args)
return runKill(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -54,16 +57,25 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error {
func runKill(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts killOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
return backend.Kill(ctx, name, api.KillOptions{
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
err = backend.Kill(ctx, name, api.KillOptions{
RemoveOrphans: opts.removeOrphans,
Project: project,
Services: services,
Signal: opts.signal,
})
if errors.Is(err, api.ErrNoResources) {
_, _ = fmt.Fprintln(stdinfo(dockerCli), "No container to kill")
return nil
}
return err
}

View File

@@ -23,12 +23,12 @@ import (
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/cli/opts"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type lsOptions struct {
@@ -38,13 +38,13 @@ type lsOptions struct {
Filter opts.FilterOpt
}
func listCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
func listCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
lsOpts := lsOptions{Filter: opts.NewFilterOpt()}
lsCmd := &cobra.Command{
Use: "ls [OPTIONS]",
Short: "List running compose projects",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runList(ctx, dockerCli, backend, lsOpts)
return runList(ctx, dockerCli, backendOptions, lsOpts)
}),
Args: cobra.NoArgs,
ValidArgsFunction: noCompletion(),
@@ -61,13 +61,17 @@ var acceptedListFilters = map[string]bool{
"name": true,
}
func runList(ctx context.Context, dockerCli command.Cli, backend api.Service, lsOpts lsOptions) error {
func runList(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, lsOpts lsOptions) error {
filters := lsOpts.Filter.Value()
err := filters.Validate(acceptedListFilters)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
stackList, err := backend.List(ctx, api.ListOptions{All: lsOpts.All})
if err != nil {
return err

View File

@@ -23,8 +23,9 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type logsOptions struct {
@@ -40,7 +41,7 @@ type logsOptions struct {
timestamps bool
}
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func logsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := logsOptions{
ProjectOptions: p,
}
@@ -48,7 +49,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
Use: "logs [OPTIONS] [SERVICE...]",
Short: "View output from containers",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runLogs(ctx, dockerCli, backend, opts, args)
return runLogs(ctx, dockerCli, backendOptions, opts, args)
}),
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.index > 0 && len(args) != 1 {
@@ -70,7 +71,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return logsCmd
}
func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error {
func runLogs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts logsOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
@@ -85,6 +86,10 @@ func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, op
}
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false)
return backend.Logs(ctx, name, consumer, api.LogOptions{
Project: project,
@@ -97,3 +102,32 @@ func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, op
Timestamps: opts.timestamps,
})
}
var _ api.LogConsumer = &logConsumer{}
type logConsumer struct {
events api.EventProcessor
}
func (l logConsumer) Log(containerName, message string) {
l.events.On(api.Resource{
ID: containerName,
Text: message,
})
}
func (l logConsumer) Err(containerName, message string) {
l.events.On(api.Resource{
ID: containerName,
Status: api.Error,
Text: message,
})
}
func (l logConsumer) Status(containerName, message string) {
l.events.On(api.Resource{
ID: containerName,
Status: api.Error,
Text: message,
})
}

View File

@@ -30,9 +30,10 @@ import (
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/tracing"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/cmd/prompt"
"github.com/docker/compose/v5/internal/tracing"
)
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
@@ -212,9 +213,9 @@ func extractEnvCLIDefined(cmdEnvs []string) map[string]string {
// Parse command-line environment variables
cmdEnvMap := make(map[string]string)
for _, env := range cmdEnvs {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
cmdEnvMap[parts[0]] = parts[1]
key, val, ok := strings.Cut(env, "=")
if ok {
cmdEnvMap[key] = val
}
}
return cmdEnvMap
@@ -247,7 +248,7 @@ func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) {
func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, options buildOptions) {
mainComposeFile := options.ProjectOptions.ConfigPaths[0] //nolint:staticcheck
if ui.Mode != ui.ModeQuiet && ui.Mode != ui.ModeJSON {
if display.Mode != display.ModeQuiet && display.Mode != display.ModeJSON {
_, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
}
}

View File

@@ -18,7 +18,6 @@ package compose
import (
"bytes"
"context"
"fmt"
"io"
"os"
@@ -28,9 +27,10 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
@@ -213,10 +213,7 @@ func TestDisplayInterpolationVariables(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create a temporary directory for the test
tmpDir, err := os.MkdirTemp("", "compose-test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tmpDir) }()
tmpDir := t.TempDir()
// Create a temporary compose file
composeContent := `
@@ -230,8 +227,7 @@ services:
- UNSET_VAR # optional without default
`
composePath := filepath.Join(tmpDir, "docker-compose.yml")
err = os.WriteFile(composePath, []byte(composeContent), 0o644)
require.NoError(t, err)
require.NoError(t, os.WriteFile(composePath, []byte(composeContent), 0o644))
buf := new(bytes.Buffer)
cli := mocks.NewMockCli(ctrl)
@@ -243,16 +239,11 @@ services:
}
// Set up the context with necessary environment variables
ctx := context.Background()
_ = os.Setenv("TEST_VAR", "test-value")
_ = os.Setenv("API_KEY", "123456")
defer func() {
_ = os.Unsetenv("TEST_VAR")
_ = os.Unsetenv("API_KEY")
}()
t.Setenv("TEST_VAR", "test-value")
t.Setenv("API_KEY", "123456")
// Extract variables from the model
info, noVariables, err := extractInterpolationVariablesFromModel(ctx, cli, projectOptions, []string{})
info, noVariables, err := extractInterpolationVariablesFromModel(t.Context(), cli, projectOptions, []string{})
require.NoError(t, err)
require.False(t, noVariables)

View File

@@ -22,14 +22,15 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type pauseOptions struct {
*ProjectOptions
}
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := pauseOptions{
ProjectOptions: p,
}
@@ -37,19 +38,23 @@ func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
Use: "pause [SERVICE...]",
Short: "Pause services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPause(ctx, dockerCli, backend, opts, args)
return runPause(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
return cmd
}
func runPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pauseOptions, services []string) error {
func runPause(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Pause(ctx, name, api.PauseOptions{
Services: services,
Project: project,
@@ -60,7 +65,7 @@ type unpauseOptions struct {
*ProjectOptions
}
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := unpauseOptions{
ProjectOptions: p,
}
@@ -68,19 +73,23 @@ func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
Use: "unpause [SERVICE...]",
Short: "Unpause services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runUnPause(ctx, dockerCli, backend, opts, args)
return runUnPause(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
return cmd
}
func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Service, opts unpauseOptions, services []string) error {
func runUnPause(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts unpauseOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.UnPause(ctx, name, api.PauseOptions{
Services: services,
Project: project,

View File

@@ -25,7 +25,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type portOptions struct {
@@ -35,7 +36,7 @@ type portOptions struct {
index int
}
func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func portCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := portOptions{
ProjectOptions: p,
}
@@ -53,7 +54,7 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPort(ctx, dockerCli, backend, opts, args[0])
return runPort(ctx, dockerCli, backendOptions, opts, args[0])
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -62,11 +63,16 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error {
func runPort(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts portOptions, service string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{
Protocol: opts.protocol,
Index: opts.index,

View File

@@ -24,13 +24,14 @@ import (
"sort"
"strings"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/cli/cli/command"
cliformatter "github.com/docker/cli/cli/command/formatter"
cliflags "github.com/docker/cli/cli/flags"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type psOptions struct {
@@ -49,22 +50,22 @@ func (p *psOptions) parseFilter() error {
if p.Filter == "" {
return nil
}
parts := strings.SplitN(p.Filter, "=", 2)
if len(parts) != 2 {
key, val, ok := strings.Cut(p.Filter, "=")
if !ok {
return errors.New("arguments to --filter should be in form KEY=VAL")
}
switch parts[0] {
switch key {
case "status":
p.Status = append(p.Status, parts[1])
p.Status = append(p.Status, val)
return nil
case "source":
return api.ErrNotImplemented
default:
return fmt.Errorf("unknown filter %s", parts[0])
return fmt.Errorf("unknown filter %s", key)
}
return nil
}
func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func psCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := psOptions{
ProjectOptions: p,
}
@@ -75,7 +76,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
return opts.parseFilter()
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPs(ctx, dockerCli, backend, args, opts)
return runPs(ctx, dockerCli, backendOptions, args, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -91,7 +92,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
return psCmd
}
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
func runPs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, services []string, opts psOptions) error { //nolint:gocyclo
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
@@ -111,6 +112,10 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
}
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
containers, err := backend.Ps(ctx, name, api.PsOptions{
Project: project,
All: opts.All || len(opts.Status) != 0,

View File

@@ -1,87 +0,0 @@
/*
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"
"path/filepath"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestPsTable(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()
out := filepath.Join(dir, "output.txt")
f, err := os.Create(out)
if err != nil {
t.Fatal("could not create output file")
}
defer func() { _ = f.Close() }()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
backend := mocks.NewMockService(ctrl)
backend.EXPECT().
Ps(gomock.Eq(ctx), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
return []api.ContainerSummary{
{
ID: "abc123",
Name: "ABC",
Image: "foo/bar",
Publishers: api.PortPublishers{
{
TargetPort: 8080,
PublishedPort: 8080,
Protocol: "tcp",
},
{
TargetPort: 8443,
PublishedPort: 8443,
Protocol: "tcp",
},
},
},
}, nil
}).AnyTimes()
opts := psOptions{ProjectOptions: &ProjectOptions{ProjectName: "test"}}
stdout := streams.NewOut(f)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(stdout).AnyTimes()
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
err = runPs(ctx, cli, backend, nil, opts)
require.NoError(t, err)
_, err = f.Seek(0, 0)
require.NoError(t, err)
output, err := os.ReadFile(out)
require.NoError(t, err)
assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
}

View File

@@ -22,10 +22,12 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type publishOptions struct {
@@ -34,9 +36,11 @@ type publishOptions struct {
ociVersion string
withEnvironment bool
assumeYes bool
app bool
insecureRegistry bool
}
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func publishCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := publishOptions{
ProjectOptions: p,
}
@@ -44,7 +48,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
Use: "publish [OPTIONS] REPOSITORY[:TAG]",
Short: "Publish compose application",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPublish(ctx, dockerCli, backend, opts, args[0])
return runPublish(ctx, dockerCli, backendOptions, opts, args[0])
}),
Args: cli.ExactArgs(1),
}
@@ -53,6 +57,8 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
flags.StringVar(&opts.ociVersion, "oci-version", "", "OCI image/artifact specification version (automatically determined by default)")
flags.BoolVar(&opts.withEnvironment, "with-env", false, "Include environment variables in the published OCI artifact")
flags.BoolVarP(&opts.assumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts`)
flags.BoolVar(&opts.app, "app", false, "Published compose application (includes referenced images)")
flags.BoolVar(&opts.insecureRegistry, "insecure-registry", false, "Use insecure registry")
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
@@ -61,12 +67,23 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
}
return pflag.NormalizedName(name)
})
// Should **only** be used for testing purpose, we don't want to promote use of insecure registries
_ = flags.MarkHidden("insecure-registry")
return cmd
}
func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error {
project, metrics, err := opts.ToProject(ctx, dockerCli, nil)
func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts publishOptions, repository string) error {
if opts.assumeYes {
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, metrics, err := opts.ToProject(ctx, dockerCli, backend, nil)
if err != nil {
return err
}
@@ -76,9 +93,10 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service,
}
return backend.Publish(ctx, project, repository, api.PublishOptions{
ResolveImageDigests: opts.resolveImageDigests,
ResolveImageDigests: opts.resolveImageDigests || opts.app,
Application: opts.app,
OCIVersion: api.OCIVersion(opts.ociVersion),
WithEnvironment: opts.withEnvironment,
AssumeYes: opts.assumeYes,
InsecureRegistry: opts.insecureRegistry,
})
}

View File

@@ -27,7 +27,8 @@ import (
"github.com/morikuni/aec"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type pullOptions struct {
@@ -42,7 +43,7 @@ type pullOptions struct {
policy string
}
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func pullCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := pullOptions{
ProjectOptions: p,
}
@@ -59,7 +60,7 @@ func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return nil
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPull(ctx, dockerCli, backend, opts, args)
return runPull(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -97,8 +98,13 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
return project, nil
}
func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
func runPull(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pullOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := opts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}

View File

@@ -23,7 +23,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type pushOptions struct {
@@ -34,7 +35,7 @@ type pushOptions struct {
Quiet bool
}
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func pushCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := pushOptions{
ProjectOptions: p,
}
@@ -42,7 +43,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
Use: "push [OPTIONS] [SERVICE...]",
Short: "Push service images",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPush(ctx, dockerCli, backend, opts, args)
return runPush(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -53,8 +54,13 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return pushCmd
}
func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services)
func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pushOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}

View File

@@ -18,10 +18,14 @@ package compose
import (
"context"
"errors"
"fmt"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type removeOptions struct {
@@ -31,7 +35,7 @@ type removeOptions struct {
volumes bool
}
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func removeCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := removeOptions{
ProjectOptions: p,
}
@@ -45,7 +49,7 @@ can override this with -v. To list all volumes, use "docker volume ls".
Any data which is not in a volume will be lost.`,
RunE: Adapt(func(ctx context.Context, args []string) error {
return runRemove(ctx, dockerCli, backend, opts, args)
return runRemove(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -59,17 +63,26 @@ Any data which is not in a volume will be lost.`,
return cmd
}
func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error {
func runRemove(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts removeOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
return backend.Remove(ctx, name, api.RemoveOptions{
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
err = backend.Remove(ctx, name, api.RemoveOptions{
Services: services,
Force: opts.force,
Volumes: opts.volumes,
Project: project,
Stop: opts.stop,
})
if errors.Is(err, api.ErrNoResources) {
_, _ = fmt.Fprintln(stdinfo(dockerCli), "No stopped containers")
return nil
}
return err
}

View File

@@ -23,7 +23,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type restartOptions struct {
@@ -33,7 +34,7 @@ type restartOptions struct {
noDeps bool
}
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func restartCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := restartOptions{
ProjectOptions: p,
}
@@ -44,7 +45,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
opts.timeChanged = cmd.Flags().Changed("timeout")
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runRestart(ctx, dockerCli, backend, opts, args)
return runRestart(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -55,7 +56,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
return restartCmd
}
func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error {
func runRestart(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts restartOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return err
@@ -74,6 +75,10 @@ func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service,
timeout = &timeoutValue
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Restart(ctx, name, api.RestartOptions{
Timeout: timeout,
Services: services,

View File

@@ -22,23 +22,23 @@ import (
"os"
"strings"
composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/format"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
cgo "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/mattn/go-shellwords"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/cli/cli"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/utils"
)
type runOptions struct {
@@ -50,7 +50,6 @@ type runOptions struct {
Detach bool
Remove bool
noTty bool
tty bool
interactive bool
user string
workdir string
@@ -120,8 +119,8 @@ func (options runOptions) apply(project *types.Project) (*types.Project, error)
return project, nil
}
func (options runOptions) getEnvironment() (types.Mapping, error) {
environment := types.NewMappingWithEquals(options.environment).Resolve(os.LookupEnv).ToMapping()
func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (types.Mapping, error) {
environment := types.NewMappingWithEquals(options.environment).Resolve(resolve).ToMapping()
for _, file := range options.envFiles {
f, err := os.Open(file)
if err != nil {
@@ -143,7 +142,7 @@ func (options runOptions) getEnvironment() (types.Mapping, error) {
return environment, nil
}
func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
options := runOptions{
composeOptions: &composeOptions{
ProjectOptions: p,
@@ -155,6 +154,10 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
buildOpts := buildOptions{
ProjectOptions: p,
}
// We remove the attribute from the option struct and use a dedicated var, to limit confusion and avoid anyone to use options.tty.
// The tty flag is here for convenience and let user do "docker compose run -it" the same way as they use the "docker run" command.
var ttyFlag bool
cmd := &cobra.Command{
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
Short: "Run a one-off command on a service",
@@ -178,22 +181,30 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
if cmd.Flags().Changed("no-TTY") {
return fmt.Errorf("--tty and --no-TTY can't be used together")
} else {
options.noTty = !options.tty
options.noTty = !ttyFlag
}
} else if !cmd.Flags().Changed("no-TTY") && !cmd.Flags().Changed("interactive") && !dockerCli.In().IsTerminal() {
// while `docker run` requires explicit `-it` flags, Compose enables interactive mode and TTY by default
// but when compose is used from a script that has stdin piped from another command, we just can't
// Here, we detect we run "by default" (user didn't passed explicit flags) and disable TTY allocation if
// we don't have an actual terminal to attach to for interactive mode
options.noTty = true
}
if options.quiet {
progress.Mode = progress.ModeQuiet
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
}
os.Stdout = devnull
display.Mode = display.ModeQuiet
backendOptions.Add(compose.WithEventProcessor(display.Quiet()))
}
createOpts.pullChanged = cmd.Flags().Changed("pull")
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
project, _, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithoutEnvironmentResolution)
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := p.ToProject(ctx, dockerCli, backend, []string{options.Service}, composecli.WithoutEnvironmentResolution)
if err != nil {
return err
}
@@ -238,7 +249,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
flags.BoolVar(&options.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY")
cmd.Flags().BoolVarP(&ttyFlag, "tty", "t", true, "Allocate a pseudo-TTY")
cmd.Flags().MarkHidden("tty") //nolint:errcheck
flags.SetNormalizeFunc(normalizeRunFlags)
@@ -256,7 +267,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
return pflag.NormalizedName(name)
}
func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
func runRun(ctx context.Context, backend api.Compose, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
project, err := options.apply(project)
if err != nil {
return err
@@ -273,11 +284,11 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
labels := types.Labels{}
for _, s := range options.labels {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
key, val, ok := strings.Cut(s, "=")
if !ok {
return fmt.Errorf("label must be set as KEY=VALUE")
}
labels[parts[0]] = parts[1]
labels[key] = val
}
var buildForRun *api.BuildOptions
@@ -289,7 +300,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
buildForRun = &bo
}
environment, err := options.getEnvironment()
environment, err := options.getEnvironment(project.Environment.Resolve)
if err != nil {
return err
}

View File

@@ -26,8 +26,10 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type scaleOptions struct {
@@ -35,7 +37,7 @@ type scaleOptions struct {
noDeps bool
}
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := scaleOptions{
ProjectOptions: p,
}
@@ -48,7 +50,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
if err != nil {
return err
}
return runScale(ctx, dockerCli, backend, opts, serviceTuples)
return runScale(ctx, dockerCli, backendOptions, opts, serviceTuples)
}),
ValidArgsFunction: completeScaleArgs(dockerCli, p),
}
@@ -58,9 +60,14 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return scaleCmd
}
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts scaleOptions, serviceReplicaTuples map[string]int) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
services := slices.Sorted(maps.Keys(serviceReplicaTuples))
project, _, err := opts.ToProject(ctx, dockerCli, services)
project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}

View File

@@ -18,17 +18,22 @@ package compose
import (
"context"
"time"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type startOptions struct {
*ProjectOptions
wait bool
waitTimeout int
}
func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func startCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := startOptions{
ProjectOptions: p,
}
@@ -36,22 +41,37 @@ func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
Use: "start [SERVICE...]",
Short: "Start services",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runStart(ctx, dockerCli, backend, opts, args)
return runStart(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := startCmd.Flags()
flags.BoolVar(&opts.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
flags.IntVar(&opts.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy")
return startCmd
}
func runStart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts startOptions, services []string) error {
func runStart(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts startOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
var timeout time.Duration
if opts.waitTimeout > 0 {
timeout = time.Duration(opts.waitTimeout) * time.Second
}
return backend.Start(ctx, name, api.StartOptions{
AttachTo: services,
Project: project,
Services: services,
AttachTo: services,
Project: project,
Services: services,
Wait: opts.wait,
WaitTimeout: timeout,
})
}

View File

@@ -25,7 +25,7 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
)
type statsOptions struct {

View File

@@ -23,7 +23,8 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type stopOptions struct {
@@ -32,7 +33,7 @@ type stopOptions struct {
timeout int
}
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func stopCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := stopOptions{
ProjectOptions: p,
}
@@ -43,7 +44,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
opts.timeChanged = cmd.Flags().Changed("timeout")
},
RunE: Adapt(func(ctx context.Context, args []string) error {
return runStop(ctx, dockerCli, backend, opts, args)
return runStop(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -53,7 +54,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error {
func runStop(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts stopOptions, services []string) error {
project, name, err := opts.projectOrName(ctx, dockerCli, services...)
if err != nil {
return err
@@ -64,6 +65,10 @@ func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, op
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
return backend.Stop(ctx, name, api.StopOptions{
Timeout: timeout,
Services: services,

View File

@@ -27,14 +27,15 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type topOptions struct {
*ProjectOptions
}
func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func topCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := topOptions{
ProjectOptions: p,
}
@@ -42,7 +43,7 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
Use: "top [SERVICES...]",
Short: "Display the running processes",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runTop(ctx, dockerCli, backend, opts, args)
return runTop(ctx, dockerCli, backendOptions, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -54,11 +55,16 @@ type (
topEntries map[string]string
)
func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
func runTop(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts topOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
return err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
containers, err := backend.Top(ctx, projectName, services)
if err != nil {
return err

View File

@@ -21,9 +21,10 @@ import (
"strings"
"testing"
"github.com/docker/compose/v2/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/docker/compose/v5/pkg/api"
)
var topTestCases = []struct {
@@ -202,7 +203,7 @@ var topTestCases = []struct {
}
// TestRunTopCore only tests the core functionality of runTop: formatting
// and printing of the output of (api.Service).Top().
// and printing of the output of (api.Compose).Top().
func TestRunTopCore(t *testing.T) {
t.Parallel()
@@ -321,7 +322,7 @@ func TestRunTopCore(t *testing.T) {
func trim(s string) string {
var out bytes.Buffer
for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
for line := range strings.SplitSeq(strings.TrimSpace(s), "\n") {
out.WriteString(strings.TrimSpace(line))
out.WriteRune('\n')
}

View File

@@ -31,10 +31,11 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v5/cmd/display"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
"github.com/docker/compose/v5/pkg/utils"
)
// composeOptions hold options common to `up` and `run` to run compose project
@@ -109,7 +110,7 @@ func (opts upOptions) OnExit() api.Cascade {
}
}
func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
up := upOptions{}
create := createOptions{}
build := buildOptions{ProjectOptions: p}
@@ -140,7 +141,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
return fmt.Errorf("no service selected")
}
return runUp(ctx, dockerCli, backend, create, up, build, project, services)
return runUp(ctx, dockerCli, backendOptions, create, up, build, project, services)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -165,6 +166,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")
@@ -186,6 +188,9 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
//nolint:gocyclo
func validateFlags(up *upOptions, create *createOptions) error {
if up.waitTimeout < 0 {
return fmt.Errorf("--wait-timeout must be a non-negative integer")
}
if up.exitCodeFrom != "" && !up.cascadeFail {
up.cascadeStop = true
}
@@ -223,10 +228,11 @@ func validateFlags(up *upOptions, create *createOptions) error {
return nil
}
//nolint:gocyclo
func runUp(
ctx context.Context,
dockerCli command.Cli,
backend api.Service,
backendOptions *BackendOptions,
createOptions createOptions,
upOptions upOptions,
buildOptions buildOptions,
@@ -260,7 +266,7 @@ func runUp(
if err != nil {
return err
}
bo.Services = services
bo.Services = project.ServiceNames()
bo.Deps = !upOptions.noDeps
build = &bo
}
@@ -275,7 +281,15 @@ func runUp(
Inherit: !createOptions.noInherit,
Timeout: createOptions.GetTimeout(),
QuietPull: createOptions.quietPull,
AssumeYes: createOptions.AssumeYes,
}
if createOptions.AssumeYes {
backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
if upOptions.noStart {
@@ -317,7 +331,10 @@ func runUp(
attach = attachSet.Elements()
}
timeout := time.Duration(upOptions.waitTimeout) * time.Second
var timeout time.Duration
if upOptions.waitTimeout > 0 {
timeout = time.Duration(upOptions.waitTimeout) * time.Second
}
return backend.Up(ctx, project, api.UpOptions{
Create: create,
Start: api.StartOptions{
@@ -330,7 +347,7 @@ func runUp(
WaitTimeout: timeout,
Watch: upOptions.watch,
Services: services,
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain",
NavigationMenu: upOptions.navigationMenu && display.Mode != "plain" && dockerCli.In().IsTerminal(),
},
})
}

View File

@@ -21,6 +21,8 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
)
func TestApplyScaleOpt(t *testing.T) {
@@ -48,3 +50,42 @@ func TestApplyScaleOpt(t *testing.T) {
assert.Equal(t, *bar.Scale, 3)
assert.Equal(t, *bar.Deploy.Replicas, 3)
}
func TestUpOptions_OnExit(t *testing.T) {
tests := []struct {
name string
args upOptions
want api.Cascade
}{
{
name: "no cascade",
args: upOptions{},
want: api.CascadeIgnore,
},
{
name: "cascade stop",
args: upOptions{cascadeStop: true},
want: api.CascadeStop,
},
{
name: "cascade fail",
args: upOptions{cascadeFail: true},
want: api.CascadeFail,
},
{
name: "both set - stop takes precedence",
args: upOptions{
cascadeStop: true,
cascadeFail: true,
},
want: api.CascadeStop,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.args.OnExit()
assert.Equal(t, got, tt.want)
})
}
}

View File

@@ -21,11 +21,10 @@ import (
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/internal"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/internal"
)
type versionOptions struct {

View File

@@ -21,10 +21,11 @@ import (
"testing"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/internal"
"github.com/docker/compose/v2/pkg/mocks"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/internal"
"github.com/docker/compose/v5/pkg/mocks"
)
func TestVersionCommand(t *testing.T) {

View File

@@ -23,8 +23,10 @@ import (
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type vizOptions struct {
@@ -35,7 +37,7 @@ type vizOptions struct {
indentationStr string
}
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func vizCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := vizOptions{
ProjectOptions: p,
}
@@ -51,7 +53,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
return err
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runViz(ctx, dockerCli, backend, &opts)
return runViz(ctx, dockerCli, backendOptions, &opts)
}),
}
@@ -63,9 +65,15 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
return cmd
}
func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error {
func runViz(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *vizOptions) error {
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
project, _, err := opts.ToProject(ctx, dockerCli, nil)
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := opts.ToProject(ctx, dockerCli, backend, nil)
if err != nil {
return err
}

View File

@@ -24,8 +24,10 @@ import (
"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"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type volumesOptions struct {
@@ -34,7 +36,7 @@ type volumesOptions struct {
Format string
}
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
options := volumesOptions{
ProjectOptions: p,
}
@@ -43,7 +45,7 @@ func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
Use: "volumes [OPTIONS] [SERVICE...]",
Short: "List volumes",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runVol(ctx, dockerCli, backend, args, options)
return runVol(ctx, dockerCli, backendOptions, args, options)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -54,25 +56,26 @@ func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
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...)
func runVol(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, services []string, options volumesOptions) error {
project, name, 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)
if project != nil {
names := project.ServiceNames()
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{
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
volumes, err := backend.Volumes(ctx, name, api.VolumesOptions{
Services: services,
})
if err != nil {

View File

@@ -22,8 +22,10 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type waitOptions struct {
@@ -34,7 +36,7 @@ type waitOptions struct {
downProject bool
}
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
opts := waitOptions{
ProjectOptions: p,
}
@@ -47,7 +49,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
Args: cli.RequiresMinArgs(1),
RunE: Adapt(func(ctx context.Context, services []string) error {
opts.services = services
statusCode, err = runWait(ctx, dockerCli, backend, &opts)
statusCode, err = runWait(ctx, dockerCli, backendOptions, &opts)
return err
}),
PostRun: func(cmd *cobra.Command, args []string) {
@@ -60,12 +62,16 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) {
func runWait(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *waitOptions) (int64, error) {
_, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return 0, err
}
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return 0, err
}
return backend.Wait(ctx, name, api.WaitOptions{
Services: opts.services,
DownProjectOnContainerExit: opts.downProject,

View File

@@ -21,13 +21,14 @@ import (
"fmt"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/locker"
"github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/internal/locker"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
type watchOptions struct {
@@ -36,7 +37,7 @@ type watchOptions struct {
noUp bool
}
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
watchOpts := watchOptions{
ProjectOptions: p,
}
@@ -53,7 +54,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
if cmd.Parent().Name() == "alpha" {
logrus.Warn("watch command is now available as a top level command")
}
return runWatch(ctx, dockerCli, backend, watchOpts, buildOpts, args)
return runWatch(ctx, dockerCli, backendOptions, watchOpts, buildOpts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@@ -64,8 +65,13 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return cmd
}
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
project, _, err := watchOpts.ToProject(ctx, dockerCli, services)
func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return err
}
project, _, err := watchOpts.ToProject(ctx, dockerCli, backend, services)
if err != nil {
return err
}

View File

@@ -14,7 +14,7 @@
limitations under the License.
*/
package progress
package display
import (
"github.com/morikuni/aec"

View File

@@ -14,18 +14,8 @@
limitations under the License.
*/
package progress
package display
import (
"context"
"testing"
"gotest.tools/v3/assert"
const (
DRYRUN_PREFIX = " DRY-RUN MODE - "
)
func TestNoopWriter(t *testing.T) {
todo := context.TODO()
writer := ContextWriter(todo)
assert.Equal(t, writer, &noopWriter{})
}

View File

@@ -14,18 +14,25 @@
limitations under the License.
*/
package progress
package display
import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/docker/compose/v5/pkg/api"
)
func JSON(out io.Writer) api.EventProcessor {
return &jsonWriter{
out: out,
}
}
type jsonWriter struct {
out io.Writer
done chan bool
dryRun bool
}
@@ -34,29 +41,25 @@ type jsonMessage struct {
Tail bool `json:"tail,omitempty"`
ID string `json:"id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Text string `json:"text,omitempty"`
Status string `json:"status,omitempty"`
Text string `json:"text,omitempty"`
Details string `json:"details,omitempty"`
Current int64 `json:"current,omitempty"`
Total int64 `json:"total,omitempty"`
Percent int `json:"percent,omitempty"`
}
func (p *jsonWriter) Start(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-p.done:
return nil
}
func (p *jsonWriter) Start(ctx context.Context, operation string) {
}
func (p *jsonWriter) Event(e Event) {
func (p *jsonWriter) Event(e api.Resource) {
message := &jsonMessage{
DryRun: p.dryRun,
Tail: false,
ID: e.ID,
Status: e.StatusText(),
Text: e.Text,
Status: e.StatusText,
Details: e.Details,
ParentID: e.ParentID,
Current: e.Current,
Total: e.Total,
@@ -68,29 +71,11 @@ func (p *jsonWriter) Event(e Event) {
}
}
func (p *jsonWriter) Events(events []Event) {
func (p *jsonWriter) On(events ...api.Resource) {
for _, e := range events {
p.Event(e)
}
}
func (p *jsonWriter) TailMsgf(msg string, args ...interface{}) {
message := &jsonMessage{
DryRun: p.dryRun,
Tail: true,
ID: "",
Text: fmt.Sprintf(msg, args...),
Status: "",
}
marshal, err := json.Marshal(message)
if err == nil {
_, _ = fmt.Fprintln(p.out, string(marshal))
}
}
func (p *jsonWriter) Stop() {
p.done <- true
}
func (p *jsonWriter) HasMore(bool) {
func (p *jsonWriter) Done(_ string, _ bool) {
}

62
cmd/display/json_test.go Normal file
View File

@@ -0,0 +1,62 @@
/*
Copyright 2024 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 display
import (
"bytes"
"encoding/json"
"testing"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
)
func TestJsonWriter_Event(t *testing.T) {
var out bytes.Buffer
w := &jsonWriter{
out: &out,
dryRun: true,
}
event := api.Resource{
ID: "service1",
ParentID: "project",
Status: api.Working,
Text: api.StatusCreating,
Current: 50,
Total: 100,
Percent: 50,
}
w.Event(event)
var actual jsonMessage
err := json.Unmarshal(out.Bytes(), &actual)
assert.NilError(t, err)
expected := jsonMessage{
DryRun: true,
ID: event.ID,
ParentID: event.ParentID,
Text: api.StatusCreating,
Status: "Working",
Current: event.Current,
Total: event.Total,
Percent: event.Percent,
}
assert.DeepEqual(t, expected, actual)
}

33
cmd/display/mode.go Normal file
View File

@@ -0,0 +1,33 @@
/*
Copyright 2024 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 display
// Mode define how progress should be rendered, either as ModePlain or ModeTTY
var Mode = ModeAuto
const (
// ModeAuto detect console capabilities
ModeAuto = "auto"
// ModeTTY use terminal capability for advanced rendering
ModeTTY = "tty"
// ModePlain dump raw events to output
ModePlain = "plain"
// ModeQuiet don't display events
ModeQuiet = "quiet"
// ModeJSON outputs a machine-readable JSON stream
ModeJSON = "json"
)

View File

@@ -14,53 +14,43 @@
limitations under the License.
*/
package progress
package display
import (
"context"
"fmt"
"io"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
)
func Plain(out io.Writer) api.EventProcessor {
return &plainWriter{
out: out,
}
}
type plainWriter struct {
out io.Writer
done chan bool
dryRun bool
}
func (p *plainWriter) Start(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-p.done:
return nil
}
func (p *plainWriter) Start(ctx context.Context, operation string) {
}
func (p *plainWriter) Event(e Event) {
func (p *plainWriter) Event(e api.Resource) {
prefix := ""
if p.dryRun {
prefix = api.DRYRUN_PREFIX
prefix = DRYRUN_PREFIX
}
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.StatusText)
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.Details)
}
func (p *plainWriter) Events(events []Event) {
func (p *plainWriter) On(events ...api.Resource) {
for _, e := range events {
p.Event(e)
}
}
func (p *plainWriter) TailMsgf(msg string, args ...interface{}) {
msg = fmt.Sprintf(msg, args...)
if p.dryRun {
msg = api.DRYRUN_PREFIX + msg
}
_, _ = fmt.Fprintln(p.out, msg)
}
func (p *plainWriter) Stop() {
p.done <- true
func (p *plainWriter) Done(_ string, _ bool) {
}

View File

@@ -14,24 +14,25 @@
limitations under the License.
*/
package progress
package display
import "context"
import (
"context"
"github.com/docker/compose/v5/pkg/api"
)
func Quiet() api.EventProcessor {
return &quiet{}
}
type quiet struct{}
func (q quiet) Start(_ context.Context) error {
return nil
func (q *quiet) Start(_ context.Context, _ string) {
}
func (q quiet) Stop() {
func (q *quiet) Done(_ string, _ bool) {
}
func (q quiet) Event(_ Event) {
}
func (q quiet) Events(_ []Event) {
}
func (q quiet) TailMsgf(_ string, _ ...interface{}) {
func (q *quiet) On(_ ...api.Resource) {
}

View File

@@ -14,14 +14,14 @@
limitations under the License.
*/
package progress
package display
import (
"runtime"
"time"
)
type spinner struct {
type Spinner struct {
time time.Time
index int
chars []string
@@ -29,7 +29,7 @@ type spinner struct {
done string
}
func newSpinner() *spinner {
func NewSpinner() *Spinner {
chars := []string{
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
}
@@ -40,7 +40,7 @@ func newSpinner() *spinner {
done = "-"
}
return &spinner{
return &Spinner{
index: 0,
time: time.Now(),
chars: chars,
@@ -48,7 +48,7 @@ func newSpinner() *spinner {
}
}
func (s *spinner) String() string {
func (s *Spinner) String() string {
if s.stop {
return s.done
}
@@ -61,10 +61,10 @@ func (s *spinner) String() string {
return s.chars[s.index]
}
func (s *spinner) Stop() {
func (s *Spinner) Stop() {
s.stop = true
}
func (s *spinner) Restart() {
func (s *Spinner) Restart() {
s.stop = false
}

664
cmd/display/tty.go Normal file
View File

@@ -0,0 +1,664 @@
/*
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 display
import (
"context"
"fmt"
"io"
"iter"
"slices"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/buger/goterm"
"github.com/docker/go-units"
"github.com/morikuni/aec"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/utils"
)
// Full creates an EventProcessor that render advanced UI within a terminal.
// On Start, TUI lists task with a progress timer
func Full(out io.Writer, info io.Writer, detached bool) api.EventProcessor {
return &ttyWriter{
out: out,
info: info,
tasks: map[string]*task{},
done: make(chan bool),
mtx: &sync.Mutex{},
detached: detached,
}
}
type ttyWriter struct {
out io.Writer
ids []string // tasks ids ordered as first event appeared
tasks map[string]*task
repeated bool
numLines int
done chan bool
mtx *sync.Mutex
dryRun bool // FIXME(ndeloof) (re)implement support for dry-run
operation string
ticker *time.Ticker
suspended bool
info io.Writer
detached bool
}
type task struct {
ID string
parent string // the resource this task receives updates from - other parents will be ignored
parents utils.Set[string] // all resources to depend on this task
startTime time.Time
endTime time.Time
text string
details string
status api.EventStatus
current int64
percent int
total int64
spinner *Spinner
}
func newTask(e api.Resource) task {
t := task{
ID: e.ID,
parents: utils.NewSet[string](),
startTime: time.Now(),
text: e.Text,
details: e.Details,
status: e.Status,
current: e.Current,
percent: e.Percent,
total: e.Total,
spinner: NewSpinner(),
}
if e.ParentID != "" {
t.parent = e.ParentID
t.parents.Add(e.ParentID)
}
if e.Status == api.Done || e.Status == api.Error {
t.stop()
}
return t
}
// update adjusts task state based on last received event
func (t *task) update(e api.Resource) {
if e.ParentID != "" {
t.parents.Add(e.ParentID)
// we may receive same event from distinct parents (typically: images sharing layers)
// to avoid status to flicker, only accept updates from our first declared parent
if t.parent != e.ParentID {
return
}
}
// update task based on received event
switch e.Status {
case api.Done, api.Error, api.Warning:
if t.status != e.Status {
t.stop()
}
case api.Working:
t.hasMore()
}
t.status = e.Status
t.text = e.Text
t.details = e.Details
// progress can only go up
if e.Total > t.total {
t.total = e.Total
}
if e.Current > t.current {
t.current = e.Current
}
if e.Percent > t.percent {
t.percent = e.Percent
}
}
func (t *task) stop() {
t.endTime = time.Now()
t.spinner.Stop()
}
func (t *task) hasMore() {
t.spinner.Restart()
}
func (t *task) Completed() bool {
switch t.status {
case api.Done, api.Error, api.Warning:
return true
default:
return false
}
}
func (w *ttyWriter) Start(ctx context.Context, operation string) {
w.ticker = time.NewTicker(100 * time.Millisecond)
w.operation = operation
go func() {
for {
select {
case <-ctx.Done():
// interrupted
w.ticker.Stop()
return
case <-w.done:
return
case <-w.ticker.C:
w.print()
}
}
}()
}
func (w *ttyWriter) Done(operation string, success bool) {
w.print()
w.mtx.Lock()
defer w.mtx.Unlock()
w.ticker.Stop()
w.operation = ""
w.done <- true
}
func (w *ttyWriter) On(events ...api.Resource) {
w.mtx.Lock()
defer w.mtx.Unlock()
for _, e := range events {
if e.ID == "Compose" {
_, _ = fmt.Fprintln(w.info, ErrorColor(e.Details))
continue
}
if w.operation != "start" && (e.Text == api.StatusStarted || e.Text == api.StatusStarting) && !w.detached {
// skip those events to avoid mix with container logs
continue
}
w.event(e)
}
}
func (w *ttyWriter) event(e api.Resource) {
// Suspend print while a build is in progress, to avoid collision with buildkit Display
if e.Text == api.StatusBuilding {
w.ticker.Stop()
w.suspended = true
} else if w.suspended {
w.ticker.Reset(100 * time.Millisecond)
w.suspended = false
}
if last, ok := w.tasks[e.ID]; ok {
last.update(e)
} else {
t := newTask(e)
w.tasks[e.ID] = &t
w.ids = append(w.ids, e.ID)
}
w.printEvent(e)
}
func (w *ttyWriter) printEvent(e api.Resource) {
if w.operation != "" {
// event will be displayed by progress UI on ticker's ticks
return
}
var color colorFunc
switch e.Status {
case api.Working:
color = SuccessColor
case api.Done:
color = SuccessColor
case api.Warning:
color = WarningColor
case api.Error:
color = ErrorColor
}
_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details)
}
func (w *ttyWriter) parentTasks() iter.Seq[*task] {
return func(yield func(*task) bool) {
for _, id := range w.ids { // iterate on ids to enforce a consistent order
t := w.tasks[id]
if len(t.parents) == 0 {
yield(t)
}
}
}
}
func (w *ttyWriter) childrenTasks(parent string) iter.Seq[*task] {
return func(yield func(*task) bool) {
for _, id := range w.ids { // iterate on ids to enforce a consistent order
t := w.tasks[id]
if t.parents.Has(parent) {
yield(t)
}
}
}
}
// lineData holds pre-computed formatting for a task line
type lineData struct {
spinner string // rendered spinner with color
prefix string // dry-run prefix if any
taskID string // possibly abbreviated
progress string // progress bar and size info
status string // rendered status with color
details string // possibly abbreviated
timer string // rendered timer with color
statusPad int // padding before status to align
timerPad int // padding before timer to align
statusColor colorFunc
}
func (w *ttyWriter) print() {
terminalWidth := goterm.Width()
terminalHeight := goterm.Height()
if terminalWidth <= 0 {
terminalWidth = 80
}
if terminalHeight <= 0 {
terminalHeight = 24
}
w.printWithDimensions(terminalWidth, terminalHeight)
}
func (w *ttyWriter) printWithDimensions(terminalWidth, terminalHeight int) {
w.mtx.Lock()
defer w.mtx.Unlock()
if len(w.tasks) == 0 {
return
}
up := w.numLines + 1
if !w.repeated {
up--
w.repeated = true
}
b := aec.NewBuilder(
aec.Hide, // Hide the cursor while we are printing
aec.Up(uint(up)),
aec.Column(0),
)
_, _ = fmt.Fprint(w.out, b.ANSI)
defer func() {
_, _ = fmt.Fprint(w.out, aec.Show)
}()
firstLine := fmt.Sprintf("[+] %s %d/%d", w.operation, numDone(w.tasks), len(w.tasks))
_, _ = fmt.Fprintln(w.out, firstLine)
// Collect parent tasks in original order
allTasks := slices.Collect(w.parentTasks())
// Available lines: terminal height - 2 (header line + potential "more" line)
maxLines := terminalHeight - 2
if maxLines < 1 {
maxLines = 1
}
showMore := len(allTasks) > maxLines
tasksToShow := allTasks
if showMore {
tasksToShow = allTasks[:maxLines-1] // Reserve one line for "more" message
}
// collect line data and compute timerLen
lines := make([]lineData, len(tasksToShow))
var timerLen int
for i, t := range tasksToShow {
lines[i] = w.prepareLineData(t)
if len(lines[i].timer) > timerLen {
timerLen = len(lines[i].timer)
}
}
// shorten details/taskID to fit terminal width
w.adjustLineWidth(lines, timerLen, terminalWidth)
// compute padding
w.applyPadding(lines, terminalWidth, timerLen)
// Render lines
numLines := 0
for _, l := range lines {
_, _ = fmt.Fprint(w.out, lineText(l))
numLines++
}
if showMore {
moreCount := len(allTasks) - len(tasksToShow)
moreText := fmt.Sprintf(" ... %d more", moreCount)
pad := terminalWidth - len(moreText)
if pad < 0 {
pad = 0
}
_, _ = fmt.Fprintf(w.out, "%s%s\n", moreText, strings.Repeat(" ", pad))
numLines++
}
// Clear any remaining lines from previous render
for i := numLines; i < w.numLines; i++ {
_, _ = fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
numLines++
}
w.numLines = numLines
}
func (w *ttyWriter) applyPadding(lines []lineData, terminalWidth int, timerLen int) {
var maxBeforeStatus int
for i := range lines {
l := &lines[i]
// Width before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress
beforeStatus := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
if beforeStatus > maxBeforeStatus {
maxBeforeStatus = beforeStatus
}
}
for i, l := range lines {
// Position before statusPad: space(1) + spinner(1) + prefix + space(1) + taskID + progress
beforeStatus := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
// statusPad aligns status; lineText adds 1 more space after statusPad
l.statusPad = maxBeforeStatus - beforeStatus
// Format: beforeStatus + statusPad + space(1) + status
lineLen := beforeStatus + l.statusPad + 1 + utf8.RuneCountInString(l.status)
if l.details != "" {
lineLen += 1 + utf8.RuneCountInString(l.details)
}
l.timerPad = terminalWidth - lineLen - timerLen
if l.timerPad < 1 {
l.timerPad = 1
}
lines[i] = l
}
}
func (w *ttyWriter) adjustLineWidth(lines []lineData, timerLen int, terminalWidth int) {
const minIDLen = 10
maxStatusLen := maxStatusLength(lines)
// Iteratively truncate until all lines fit
for range 100 { // safety limit
maxBeforeStatus := maxBeforeStatusWidth(lines)
overflow := computeOverflow(lines, maxBeforeStatus, maxStatusLen, timerLen, terminalWidth)
if overflow <= 0 {
break
}
// First try to truncate details, then taskID
if !truncateDetails(lines, overflow) && !truncateLongestTaskID(lines, overflow, minIDLen) {
break // Can't truncate further
}
}
}
// maxStatusLength returns the maximum status text length across all lines.
func maxStatusLength(lines []lineData) int {
var maxLen int
for i := range lines {
if len(lines[i].status) > maxLen {
maxLen = len(lines[i].status)
}
}
return maxLen
}
// maxBeforeStatusWidth computes the maximum width before statusPad across all lines.
// This is: space(1) + spinner(1) + prefix + space(1) + taskID + progress
func maxBeforeStatusWidth(lines []lineData) int {
var maxWidth int
for i := range lines {
l := &lines[i]
width := 3 + lenAnsi(l.prefix) + len(l.taskID) + lenAnsi(l.progress)
if width > maxWidth {
maxWidth = width
}
}
return maxWidth
}
// computeOverflow calculates how many characters the widest line exceeds the terminal width.
// Returns 0 or negative if all lines fit.
func computeOverflow(lines []lineData, maxBeforeStatus, maxStatusLen, timerLen, terminalWidth int) int {
var maxOverflow int
for i := range lines {
l := &lines[i]
detailsLen := len(l.details)
if detailsLen > 0 {
detailsLen++ // space before details
}
// Line width: maxBeforeStatus + space(1) + status + details + minTimerPad(1) + timer
lineWidth := maxBeforeStatus + 1 + maxStatusLen + detailsLen + 1 + timerLen
overflow := lineWidth - terminalWidth
if overflow > maxOverflow {
maxOverflow = overflow
}
}
return maxOverflow
}
// truncateDetails tries to truncate the first line's details to reduce overflow.
// Returns true if any truncation was performed.
func truncateDetails(lines []lineData, overflow int) bool {
for i := range lines {
l := &lines[i]
if len(l.details) > 3 {
reduction := overflow
if reduction > len(l.details)-3 {
reduction = len(l.details) - 3
}
l.details = l.details[:len(l.details)-reduction-3] + "..."
return true
} else if l.details != "" {
l.details = ""
return true
}
}
return false
}
// truncateLongestTaskID truncates the longest taskID to reduce overflow.
// Returns true if truncation was performed.
func truncateLongestTaskID(lines []lineData, overflow, minIDLen int) bool {
longestIdx := -1
longestLen := minIDLen
for i := range lines {
if len(lines[i].taskID) > longestLen {
longestLen = len(lines[i].taskID)
longestIdx = i
}
}
if longestIdx < 0 {
return false
}
l := &lines[longestIdx]
reduction := overflow + 3 // account for "..."
newLen := len(l.taskID) - reduction
if newLen < minIDLen-3 {
newLen = minIDLen - 3
}
if newLen > 0 {
l.taskID = l.taskID[:newLen] + "..."
}
return true
}
func (w *ttyWriter) prepareLineData(t *task) lineData {
endTime := time.Now()
if t.status != api.Working {
endTime = t.startTime
if (t.endTime != time.Time{}) {
endTime = t.endTime
}
}
prefix := ""
if w.dryRun {
prefix = PrefixColor(DRYRUN_PREFIX)
}
elapsed := endTime.Sub(t.startTime).Seconds()
var (
hideDetails bool
total int64
current int64
completion []string
)
// only show the aggregated progress while the root operation is in-progress
if t.status == api.Working {
for child := range w.childrenTasks(t.ID) {
if child.status == api.Working && child.total == 0 {
hideDetails = true
}
total += child.total
current += child.current
r := len(percentChars) - 1
p := child.percent
if p > 100 {
p = 100
}
completion = append(completion, percentChars[r*p/100])
}
}
if total == 0 {
hideDetails = true
}
var progress string
if len(completion) > 0 {
progress = " [" + SuccessColor(strings.Join(completion, "")) + "]"
if !hideDetails {
progress += fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
}
}
return lineData{
spinner: spinner(t),
prefix: prefix,
taskID: t.ID,
progress: progress,
status: t.text,
statusColor: colorFn(t.status),
details: t.details,
timer: fmt.Sprintf("%.1fs", elapsed),
}
}
func lineText(l lineData) string {
var sb strings.Builder
sb.WriteString(" ")
sb.WriteString(l.spinner)
sb.WriteString(l.prefix)
sb.WriteString(" ")
sb.WriteString(l.taskID)
sb.WriteString(l.progress)
sb.WriteString(strings.Repeat(" ", l.statusPad))
sb.WriteString(" ")
sb.WriteString(l.statusColor(l.status))
if l.details != "" {
sb.WriteString(" ")
sb.WriteString(l.details)
}
sb.WriteString(strings.Repeat(" ", l.timerPad))
sb.WriteString(TimerColor(l.timer))
sb.WriteString("\n")
return sb.String()
}
var (
spinnerDone = "✔"
spinnerWarning = "!"
spinnerError = "✘"
)
func spinner(t *task) string {
switch t.status {
case api.Done:
return SuccessColor(spinnerDone)
case api.Warning:
return WarningColor(spinnerWarning)
case api.Error:
return ErrorColor(spinnerError)
default:
return CountColor(t.spinner.String())
}
}
func colorFn(s api.EventStatus) colorFunc {
switch s {
case api.Done:
return SuccessColor
case api.Warning:
return WarningColor
case api.Error:
return ErrorColor
default:
return nocolor
}
}
func numDone(tasks map[string]*task) int {
i := 0
for _, t := range tasks {
if t.status != api.Working {
i++
}
}
return i
}
// lenAnsi count of user-perceived characters in ANSI string.
func lenAnsi(s string) int {
length := 0
ansiCode := false
for _, r := range s {
if r == '\x1b' {
ansiCode = true
continue
}
if ansiCode && r == 'm' {
ansiCode = false
continue
}
if !ansiCode {
length++
}
}
return length
}
var percentChars = strings.Split("⠀⡀⣀⣄⣤⣦⣶⣷⣿", "")

424
cmd/display/tty_test.go Normal file
View File

@@ -0,0 +1,424 @@
/*
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 display
import (
"bytes"
"strings"
"sync"
"testing"
"time"
"unicode/utf8"
"gotest.tools/v3/assert"
"github.com/docker/compose/v5/pkg/api"
)
func newTestWriter() (*ttyWriter, *bytes.Buffer) {
var buf bytes.Buffer
w := &ttyWriter{
out: &buf,
info: &buf,
tasks: map[string]*task{},
done: make(chan bool),
mtx: &sync.Mutex{},
operation: "pull",
}
return w, &buf
}
func addTask(w *ttyWriter, id, text, details string, status api.EventStatus) {
t := &task{
ID: id,
parents: make(map[string]struct{}),
startTime: time.Now(),
text: text,
details: details,
status: status,
spinner: NewSpinner(),
}
w.tasks[id] = t
w.ids = append(w.ids, id)
}
// extractLines parses the output buffer and returns lines without ANSI control sequences
func extractLines(buf *bytes.Buffer) []string {
content := buf.String()
// Split by newline
rawLines := strings.Split(content, "\n")
var lines []string
for _, line := range rawLines {
// Skip empty lines and lines that are just ANSI codes
if lenAnsi(line) > 0 {
lines = append(lines, line)
}
}
return lines
}
func TestPrintWithDimensions_LinesFitTerminalWidth(t *testing.T) {
testCases := []struct {
name string
taskID string
status string
details string
terminalWidth int
}{
{
name: "short task fits wide terminal",
taskID: "Image foo",
status: "Pulling",
details: "layer abc123",
terminalWidth: 100,
},
{
name: "long details truncated to fit",
taskID: "Image foo",
status: "Pulling",
details: "downloading layer sha256:abc123def456789xyz0123456789abcdef",
terminalWidth: 50,
},
{
name: "long taskID truncated to fit",
taskID: "very-long-image-name-that-exceeds-terminal-width",
status: "Pulling",
details: "",
terminalWidth: 40,
},
{
name: "both long taskID and details",
taskID: "my-very-long-service-name-here",
status: "Downloading",
details: "layer sha256:abc123def456789xyz0123456789",
terminalWidth: 50,
},
{
name: "narrow terminal",
taskID: "service-name",
status: "Pulling",
details: "some details",
terminalWidth: 35,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w, buf := newTestWriter()
addTask(w, tc.taskID, tc.status, tc.details, api.Working)
w.printWithDimensions(tc.terminalWidth, 24)
lines := extractLines(buf)
for i, line := range lines {
lineLen := lenAnsi(line)
assert.Assert(t, lineLen <= tc.terminalWidth,
"line %d has length %d which exceeds terminal width %d: %q",
i, lineLen, tc.terminalWidth, line)
}
})
}
}
func TestPrintWithDimensions_MultipleTasksFitTerminalWidth(t *testing.T) {
w, buf := newTestWriter()
// Add multiple tasks with varying lengths
addTask(w, "Image nginx", "Pulling", "layer sha256:abc123", api.Working)
addTask(w, "Image postgres-database", "Pulling", "downloading", api.Working)
addTask(w, "Image redis", "Pulled", "", api.Done)
terminalWidth := 60
w.printWithDimensions(terminalWidth, 24)
lines := extractLines(buf)
for i, line := range lines {
lineLen := lenAnsi(line)
assert.Assert(t, lineLen <= terminalWidth,
"line %d has length %d which exceeds terminal width %d: %q",
i, lineLen, terminalWidth, line)
}
}
func TestPrintWithDimensions_VeryNarrowTerminal(t *testing.T) {
w, buf := newTestWriter()
addTask(w, "Image nginx", "Pulling", "details", api.Working)
terminalWidth := 30
w.printWithDimensions(terminalWidth, 24)
lines := extractLines(buf)
for i, line := range lines {
lineLen := lenAnsi(line)
assert.Assert(t, lineLen <= terminalWidth,
"line %d has length %d which exceeds terminal width %d: %q",
i, lineLen, terminalWidth, line)
}
}
func TestPrintWithDimensions_TaskWithProgress(t *testing.T) {
w, buf := newTestWriter()
// Create parent task
parent := &task{
ID: "Image nginx",
parents: make(map[string]struct{}),
startTime: time.Now(),
text: "Pulling",
status: api.Working,
spinner: NewSpinner(),
}
w.tasks["Image nginx"] = parent
w.ids = append(w.ids, "Image nginx")
// Create child tasks to trigger progress display
for i := 0; i < 3; i++ {
child := &task{
ID: "layer" + string(rune('a'+i)),
parents: map[string]struct{}{"Image nginx": {}},
startTime: time.Now(),
text: "Downloading",
status: api.Working,
total: 1000,
current: 500,
percent: 50,
spinner: NewSpinner(),
}
w.tasks[child.ID] = child
w.ids = append(w.ids, child.ID)
}
terminalWidth := 80
w.printWithDimensions(terminalWidth, 24)
lines := extractLines(buf)
for i, line := range lines {
lineLen := lenAnsi(line)
assert.Assert(t, lineLen <= terminalWidth,
"line %d has length %d which exceeds terminal width %d: %q",
i, lineLen, terminalWidth, line)
}
}
func TestAdjustLineWidth_DetailsCorrectlyTruncated(t *testing.T) {
w := &ttyWriter{}
lines := []lineData{
{
taskID: "Image foo",
status: "Pulling",
details: "downloading layer sha256:abc123def456789xyz",
},
}
terminalWidth := 50
timerLen := 5
w.adjustLineWidth(lines, timerLen, terminalWidth)
// Verify the line fits
detailsLen := len(lines[0].details)
if detailsLen > 0 {
detailsLen++ // space before details
}
// widthWithoutDetails = 5 + prefix(0) + taskID(9) + progress(0) + status(7) + timer(5) = 26
lineWidth := 5 + len(lines[0].taskID) + len(lines[0].status) + detailsLen + timerLen
assert.Assert(t, lineWidth <= terminalWidth,
"line width %d should not exceed terminal width %d (taskID=%q, details=%q)",
lineWidth, terminalWidth, lines[0].taskID, lines[0].details)
// Verify details were truncated (not removed entirely)
assert.Assert(t, lines[0].details != "", "details should be truncated, not removed")
assert.Assert(t, strings.HasSuffix(lines[0].details, "..."), "truncated details should end with ...")
}
func TestAdjustLineWidth_TaskIDCorrectlyTruncated(t *testing.T) {
w := &ttyWriter{}
lines := []lineData{
{
taskID: "very-long-image-name-that-exceeds-minimum-length",
status: "Pulling",
details: "",
},
}
terminalWidth := 40
timerLen := 5
w.adjustLineWidth(lines, timerLen, terminalWidth)
lineWidth := 5 + len(lines[0].taskID) + 7 + timerLen
assert.Assert(t, lineWidth <= terminalWidth,
"line width %d should not exceed terminal width %d (taskID=%q)",
lineWidth, terminalWidth, lines[0].taskID)
assert.Assert(t, strings.HasSuffix(lines[0].taskID, "..."), "truncated taskID should end with ...")
}
func TestAdjustLineWidth_NoTruncationNeeded(t *testing.T) {
w := &ttyWriter{}
originalDetails := "short"
originalTaskID := "Image foo"
lines := []lineData{
{
taskID: originalTaskID,
status: "Pulling",
details: originalDetails,
},
}
// Wide terminal, nothing should be truncated
w.adjustLineWidth(lines, 5, 100)
assert.Equal(t, originalTaskID, lines[0].taskID, "taskID should not be modified")
assert.Equal(t, originalDetails, lines[0].details, "details should not be modified")
}
func TestAdjustLineWidth_DetailsRemovedWhenTooShort(t *testing.T) {
w := &ttyWriter{}
lines := []lineData{
{
taskID: "Image foo",
status: "Pulling",
details: "abc", // Very short, can't be meaningfully truncated
},
}
// Terminal so narrow that even minimal details + "..." wouldn't help
w.adjustLineWidth(lines, 5, 28)
assert.Equal(t, "", lines[0].details, "details should be removed entirely when too short to truncate")
}
// stripAnsi removes ANSI escape codes from a string
func stripAnsi(s string) string {
var result strings.Builder
inAnsi := false
for _, r := range s {
if r == '\x1b' {
inAnsi = true
continue
}
if inAnsi {
// ANSI sequences end with a letter (m, h, l, G, etc.)
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
inAnsi = false
}
continue
}
result.WriteRune(r)
}
return result.String()
}
func TestPrintWithDimensions_PulledAndPullingWithLongIDs(t *testing.T) {
w, buf := newTestWriter()
// Add a completed task with long ID
completedTask := &task{
ID: "Image docker.io/library/nginx-long-name",
parents: make(map[string]struct{}),
startTime: time.Now().Add(-2 * time.Second),
endTime: time.Now(),
text: "Pulled",
status: api.Done,
spinner: NewSpinner(),
}
completedTask.spinner.Stop()
w.tasks[completedTask.ID] = completedTask
w.ids = append(w.ids, completedTask.ID)
// Add a pending task with long ID
pendingTask := &task{
ID: "Image docker.io/library/postgres-database",
parents: make(map[string]struct{}),
startTime: time.Now(),
text: "Pulling",
status: api.Working,
spinner: NewSpinner(),
}
w.tasks[pendingTask.ID] = pendingTask
w.ids = append(w.ids, pendingTask.ID)
terminalWidth := 50
w.printWithDimensions(terminalWidth, 24)
// Strip all ANSI codes from output and split by newline
stripped := stripAnsi(buf.String())
lines := strings.Split(stripped, "\n")
// Filter non-empty lines
var nonEmptyLines []string
for _, line := range lines {
if strings.TrimSpace(line) != "" {
nonEmptyLines = append(nonEmptyLines, line)
}
}
// Expected output format (50 runes per task line)
expected := `[+] pull 1/2
✔ Image docker.io/library/nginx-l... Pulled 2.0s
⠋ Image docker.io/library/postgre... Pulling 0.0s`
expectedLines := strings.Split(expected, "\n")
// Debug output
t.Logf("Actual output:\n")
for i, line := range nonEmptyLines {
t.Logf(" line %d (%2d runes): %q", i, utf8.RuneCountInString(line), line)
}
// Verify number of lines
assert.Equal(t, len(expectedLines), len(nonEmptyLines), "number of lines should match")
// Verify each line matches expected
for i, line := range nonEmptyLines {
if i < len(expectedLines) {
assert.Equal(t, expectedLines[i], line,
"line %d should match expected", i)
}
}
// Verify task lines fit within terminal width (strict - no tolerance)
for i, line := range nonEmptyLines {
if i > 0 { // Skip header line
runeCount := utf8.RuneCountInString(line)
assert.Assert(t, runeCount <= terminalWidth,
"line %d has %d runes which exceeds terminal width %d: %q",
i, runeCount, terminalWidth, line)
}
}
}
func TestLenAnsi(t *testing.T) {
testCases := []struct {
input string
expected int
}{
{"hello", 5},
{"\x1b[32mhello\x1b[0m", 5},
{"\x1b[1;32mgreen\x1b[0m text", 10},
{"", 0},
{"\x1b[0m", 0},
}
for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := lenAnsi(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}

View File

@@ -20,81 +20,73 @@ import (
"fmt"
"github.com/acarl005/stripansi"
"github.com/morikuni/aec"
)
var disableAnsi bool
func ansi(code string) string {
return fmt.Sprintf("\033%s", code)
}
func SaveCursor() {
func saveCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("7"))
// see https://github.com/morikuni/aec/pull/5
fmt.Print(aec.Save)
}
func RestoreCursor() {
func restoreCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("8"))
// see https://github.com/morikuni/aec/pull/5
fmt.Print(aec.Restore)
}
func HideCursor() {
func showCursor() {
if disableAnsi {
return
}
fmt.Print(ansi("[?25l"))
fmt.Print(aec.Show)
}
func ShowCursor() {
func moveCursor(y, x int) {
if disableAnsi {
return
}
fmt.Print(ansi("[?25h"))
fmt.Print(aec.Position(uint(y), uint(x)))
}
func MoveCursor(y, x int) {
func carriageReturn() {
if disableAnsi {
return
}
fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
fmt.Print(aec.Column(0))
}
func MoveCursorX(pos int) {
if disableAnsi {
return
}
fmt.Print(ansi(fmt.Sprintf("[%dG", pos)))
}
func ClearLine() {
func clearLine() {
if disableAnsi {
return
}
// Does not move cursor from its current position
fmt.Print(ansi("[2K"))
fmt.Print(aec.EraseLine(aec.EraseModes.Tail))
}
func MoveCursorUp(lines int) {
func moveCursorUp(lines int) {
if disableAnsi {
return
}
// Does not add new lines
fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
fmt.Print(aec.Up(uint(lines)))
}
func MoveCursorDown(lines int) {
func moveCursorDown(lines int) {
if disableAnsi {
return
}
// Does not add new lines
fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
fmt.Print(aec.Down(uint(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

@@ -22,6 +22,7 @@ const (
// TemplateLegacyJSON the legacy json formatting value using go template
TemplateLegacyJSON = "{{json.}}"
// PRETTY is the constant for default formats on list commands
//
// Deprecated: use TABLE
PRETTY = "pretty"
// TABLE Print output in table format with column headers (default)

View File

@@ -23,10 +23,11 @@ import (
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/docker/compose/v5/pkg/api"
)
const (
@@ -104,7 +105,7 @@ type ContainerContext struct {
// used in the template. It's currently only used to detect use of the .Size
// field which (if used) automatically sets the '--size' option when making
// the API call.
FieldsUsed map[string]interface{}
FieldsUsed map[string]any
}
// NewContainerContext creates a new context for rendering containers
@@ -273,7 +274,7 @@ func (c *ContainerContext) Networks() string {
// Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
func (c *ContainerContext) Size() string {
if c.FieldsUsed == nil {
c.FieldsUsed = map[string]interface{}{}
c.FieldsUsed = map[string]any{}
}
c.FieldsUsed["Size"] = struct{}{}
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)

View File

@@ -22,11 +22,11 @@ import (
"reflect"
"strings"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v5/pkg/api"
)
// Print prints formatted lists in different formats
func Print(toJSON interface{}, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error {
func Print(toJSON any, format string, outWriter io.Writer, writerFn func(w io.Writer), headers ...string) error {
switch strings.ToLower(format) {
case TABLE, PRETTY, "":
return PrintPrettySection(outWriter, writerFn, headers...)

View File

@@ -45,7 +45,7 @@ func TestPrint(t *testing.T) {
}
b := &bytes.Buffer{}
assert.NilError(t, Print(testList, PRETTY, b, func(w io.Writer) {
assert.NilError(t, Print(testList, TABLE, b, func(w io.Writer) {
for _, t := range testList {
_, _ = fmt.Fprintf(w, "%s\t%s\n", t.Name, t.Status)
}

View File

@@ -24,12 +24,12 @@ import (
const standardIndentation = " "
// ToStandardJSON return a string with the JSON representation of the interface{}
func ToStandardJSON(i interface{}) (string, error) {
func ToStandardJSON(i any) (string, error) {
return ToJSON(i, "", standardIndentation)
}
// ToJSON return a string with the JSON representation of the interface{}
func ToJSON(i interface{}, prefix string, indentation string) (string, error) {
func ToJSON(i any, prefix string, indentation string) (string, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)

View File

@@ -26,8 +26,9 @@ import (
"time"
"github.com/buger/goterm"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/compose/v5/pkg/api"
)
// LogConsumer consume logs from services and format them
@@ -56,10 +57,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 +70,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()
}
}
@@ -87,7 +87,7 @@ func (l *logConsumer) register(name string) *presenter {
l.presenters.Store(name, p)
l.computeWidth()
if l.prefix {
l.presenters.Range(func(key, value interface{}) bool {
l.presenters.Range(func(key, value any) bool {
p := value.(*presenter)
p.setPrefix(l.width)
return true
@@ -120,9 +120,9 @@ func (l *logConsumer) write(w io.Writer, container, message string) {
}
p := l.getPresenter(container)
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
for _, line := range strings.Split(message, "\n") {
for line := range strings.SplitSeq(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)
}
@@ -137,7 +137,7 @@ func (l *logConsumer) Status(container, msg string) {
func (l *logConsumer) computeWidth() {
width := 0
l.presenters.Range(func(key, value interface{}) bool {
l.presenters.Range(func(key, value any) bool {
p := value.(*presenter)
if len(p.name) > width {
width = len(p.name)
@@ -184,7 +184,3 @@ func (l logDecorator) Status(container, msg string) {
l.decorated.Status(container, msg)
l.After()
}
func (l logDecorator) Register(container string) {
l.decorated.Register(container)
}

View File

@@ -22,15 +22,17 @@ import (
"fmt"
"math"
"os"
"strings"
"syscall"
"time"
"github.com/buger/goterm"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/eiannone/keyboard"
"github.com/skratchdot/open-golang/open"
"github.com/docker/compose/v5/internal/tracing"
"github.com/docker/compose/v5/pkg/api"
)
const DISPLAY_ERROR_TIME = 10
@@ -48,8 +50,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)
}
@@ -90,6 +92,7 @@ const (
type LogKeyboard struct {
kError KeyboardError
Watch *KeyboardWatch
Detach func()
IsDockerDesktopActive bool
logLevel KEYBOARD_LOG_LEVEL
signalChannel chan<- os.Signal
@@ -133,7 +136,7 @@ func (lk *LogKeyboard) createBuffer(lines int) {
if lines > 0 {
allocateSpace(lines)
MoveCursorUp(lines)
moveCursorUp(lines)
}
}
@@ -146,57 +149,51 @@ 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()
}
}
func (lk *LogKeyboard) navigationMenu() string {
var openDDInfo string
var items []string
if lk.IsDockerDesktopActive {
openDDInfo = shortcutKeyColor("v") + navColor(" View in Docker Desktop")
items = append(items, shortcutKeyColor("v")+navColor(" View in Docker Desktop"))
}
var openDDUI string
if openDDInfo != "" {
openDDUI = navColor(" ")
}
if lk.IsDockerDesktopActive {
openDDUI = openDDUI + shortcutKeyColor("o") + navColor(" View Config")
items = append(items, shortcutKeyColor("o")+navColor(" View Config"))
}
var watchInfo string
if openDDInfo != "" || openDDUI != "" {
watchInfo = navColor(" ")
}
isEnabled := " Enable"
if lk.Watch != nil && lk.Watch.Watching {
isEnabled = " Disable"
}
watchInfo = watchInfo + shortcutKeyColor("w") + navColor(isEnabled+" Watch")
return openDDInfo + openDDUI + watchInfo
items = append(items, shortcutKeyColor("w")+navColor(isEnabled+" Watch"))
items = append(items, shortcutKeyColor("d")+navColor(" Detach"))
return strings.Join(items, " ")
}
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) {
@@ -290,6 +287,9 @@ func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
switch kRune := event.Rune; kRune {
case 'd':
lk.clearNavigationMenu()
lk.Detach()
case 'v':
lk.openDockerDesktop(ctx, project)
case 'w':
@@ -316,13 +316,15 @@ 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.KeyCtrlZ:
handleCtrlZ()
case keyboard.KeyEnter:
NewLine()
newLine()
lk.printNavigationMenu()
}
}
@@ -334,11 +336,15 @@ func (lk *LogKeyboard) EnableWatch(enabled bool, watcher Feature) {
}
}
func (lk *LogKeyboard) EnableDetach(detach func()) {
lk.Detach = detach
}
func allocateSpace(lines int) {
for i := 0; i < lines; i++ {
ClearLine()
NewLine()
MoveCursorX(0)
clearLine()
newLine()
carriageReturn()
}
}

View File

@@ -1,5 +1,7 @@
//go:build !windows
/*
Copyright 2020 Docker Compose CLI authors
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,26 +16,10 @@
limitations under the License.
*/
package progress
package formatter
import (
"context"
)
import "syscall"
type noopWriter struct{}
func (p *noopWriter) Start(ctx context.Context) error {
return nil
}
func (p *noopWriter) Event(Event) {
}
func (p *noopWriter) Events([]Event) {
}
func (p *noopWriter) TailMsgf(_ string, _ ...interface{}) {
}
func (p *noopWriter) Stop() {
func handleCtrlZ() {
_ = syscall.Kill(0, syscall.SIGSTOP)
}

View File

@@ -0,0 +1,25 @@
//go:build windows
/*
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package formatter
// handleCtrlZ is a no-op on Windows as SIGSTOP is not supported
func handleCtrlZ() {
// Windows doesn't support SIGSTOP/SIGCONT signals
// Ctrl+Z behavior is handled differently by the Windows terminal
}

View File

@@ -20,75 +20,61 @@ 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"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/compatibility"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/docker/compose/v2/internal"
"github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v5/cmd/cmdtrace"
"github.com/docker/compose/v5/cmd/compatibility"
commands "github.com/docker/compose/v5/cmd/compose"
"github.com/docker/compose/v5/cmd/prompt"
"github.com/docker/compose/v5/internal"
"github.com/docker/compose/v5/pkg/compose"
)
func pluginMain() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
// TODO(milas): this cast is safe but we should not need to do this,
// we should expose the concrete service type so that we do not need
// to rely on the `api.Service` interface internally
backend := compose.NewComposeService(dockerCli).(commands.Backend)
cmd := commands.RootCommand(dockerCli, backend)
originalPreRunE := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// initialize the dockerCli instance
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
// compose-specific initialization
dockerCliPostInitialize(dockerCli)
if err := cmdtrace.Setup(cmd, dockerCli, os.Args[1:]); err != nil {
logrus.Debugf("failed to enable tracing: %v", err)
plugin.Run(
func(cli command.Cli) *cobra.Command {
backendOptions := &commands.BackendOptions{
Options: []compose.Option{
compose.WithPrompt(prompt.NewPrompt(cli.In(), cli.Out()).Confirm),
},
}
if originalPreRunE != nil {
return originalPreRunE(cmd, args)
}
return nil
}
cmd := commands.RootCommand(cli, backendOptions)
originalPreRunE := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// initialize the cli instance
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
if err := cmdtrace.Setup(cmd, cli, os.Args[1:]); err != nil {
logrus.Debugf("failed to enable tracing: %v", err)
}
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
return dockercli.StatusError{
StatusCode: 1,
Status: err.Error(),
if originalPreRunE != nil {
return originalPreRunE(cmd, args)
}
return nil
}
})
return cmd
},
manager.Metadata{
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
return dockercli.StatusError{
StatusCode: 1,
Status: err.Error(),
}
})
return cmd
},
metadata.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: internal.Version,
})
}
// dockerCliPostInitialize performs Compose-specific configuration for the
// command.Cli instance provided by the plugin.Run() initialization.
//
// NOTE: This must be called AFTER plugin.PersistentPreRunE.
func dockerCliPostInitialize(dockerCli command.Cli) {
// HACK(milas): remove once docker/cli#4574 is merged; for now,
// set it in a rather roundabout way by grabbing the underlying
// concrete client and manually invoking an option on it
_ = dockerCli.Apply(func(cli *command.DockerCli) error {
if mobyClient, ok := cli.Client().(*client.Client); ok {
_ = client.WithUserAgent("compose/" + internal.Version)(mobyClient)
}
return nil
})
},
command.WithUserAgent("compose/"+internal.Version),
)
}
func main() {

View File

@@ -22,10 +22,11 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v5/pkg/utils"
)
//go:generate mockgen -destination=./prompt_mock.go -self_package "github.com/docker/compose/v2/pkg/prompt" -package=prompt . UI
//go:generate mockgen -destination=./prompt_mock.go -self_package "github.com/docker/compose/v5/pkg/prompt" -package=prompt . UI
// UI - prompt user input
type UI interface {

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Container: github.com/docker/compose-cli/pkg/prompt (interfaces: UI)
// Source: github.com/docker/compose-cli/pkg/prompt (interfaces: UI)
// Package prompt is a generated GoMock package.
package prompt

View File

@@ -1,7 +1,7 @@
# About
The Compose application model defines `service` as an abstraction for a computing unit managing (a subset of)
application needs, which can interact with other service by relying on network(s). Docker Compose is designed
application needs, which can interact with other services by relying on network(s). Docker Compose is designed
to use the Docker Engine ("Moby") API to manage services as containers, but the abstraction _could_ also cover
many other runtimes, typically cloud services or services natively provided by host.
@@ -55,8 +55,8 @@ JSON messages MUST include a `type` and a `message` attribute.
`type` can be either:
- `info`: Reports status updates to the user. Compose will render message as the service state in the progress UI
- `error`: Let's the user know something went wrong with details about the error. Compose will render the message as the reason for the service failure.
- `setenv`: Let's the plugin tell Compose how dependent services can access the created resource. See next section for further details.
- `error`: Lets the user know something went wrong with details about the error. Compose will render the message as the reason for the service failure.
- `setenv`: Lets the plugin tell Compose how dependent services can access the created resource. See next section for further details.
- `debug`: Those messages could help debugging the provider, but are not rendered to the user by default. They are rendered when Compose is started with `--verbose` flag.
```mermaid

View File

@@ -1,3 +1,4 @@
# docker compose
```text
@@ -126,6 +127,57 @@ get the postgres image for the db service from anywhere by using the `-f` flag a
$ docker compose -f ~/sandbox/rails/compose.yaml pull db
```
#### Using an OCI published artifact
You can use the `-f` flag with the `oci://` prefix to reference a Compose file that has been published to an OCI registry.
This allows you to distribute and version your Compose configurations as OCI artifacts.
To use a Compose file from an OCI registry:
```console
$ docker compose -f oci://registry.example.com/my-compose-project:latest up
```
You can also combine OCI artifacts with local files:
```console
$ docker compose -f oci://registry.example.com/my-compose-project:v1.0 -f compose.override.yaml up
```
The OCI artifact must contain a valid Compose file. You can publish Compose files to an OCI registry using the
`docker compose publish` command.
#### Using a git repository
You can use the `-f` flag to reference a Compose file from a git repository. Compose supports various git URL formats:
Using HTTPS:
```console
$ docker compose -f https://github.com/user/repo.git up
```
Using SSH:
```console
$ docker compose -f git@github.com:user/repo.git up
```
You can specify a specific branch, tag, or commit:
```console
$ docker compose -f https://github.com/user/repo.git@main up
$ docker compose -f https://github.com/user/repo.git@v1.0.0 up
$ docker compose -f https://github.com/user/repo.git@abc123 up
```
You can also specify a subdirectory within the repository:
```console
$ docker compose -f https://github.com/user/repo.git#main:path/to/compose.yaml up
```
When using git resources, Compose will clone the repository and use the specified Compose file. You can combine
git resources with local files:
```console
$ docker compose -f https://github.com/user/repo.git -f compose.override.yaml up
```
### Use `-p` to specify a project name
Each configuration has a project name. Compose sets the project name using

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,7 @@ 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 |

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

@@ -6,6 +6,12 @@ This is the equivalent of `docker exec` targeting a Compose service.
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
a script.
### Options
| Name | Type | Default | Description |
@@ -14,7 +20,7 @@ you can use a command such as `docker compose exec web sh` to get an interactive
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-e`, `--env` | `stringArray` | | Set environment variables |
| `--index` | `int` | `0` | Index of the container if service has multiple replicas |
| `-T`, `--no-TTY` | `bool` | `true` | Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY. |
| `-T`, `--no-tty` | `bool` | `true` | Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY. |
| `--privileged` | `bool` | | Give extended privileges to the process |
| `-u`, `--user` | `string` | | Run the command as this user |
| `-w`, `--workdir` | `string` | | Path to workdir directory for this command |
@@ -28,3 +34,9 @@ This is the equivalent of `docker exec` targeting a Compose service.
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
a script.

View File

@@ -7,6 +7,7 @@ Publish compose application
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
| `--app` | `bool` | | Published compose application (includes referenced images) |
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) |
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |

View File

@@ -5,9 +5,11 @@ Starts existing containers for a service
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| Name | Type | Default | Description |
|:-----------------|:-------|:--------|:---------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--wait` | `bool` | | Wait for services to be running\|healthy. Implies detached mode. |
| `--wait-timeout` | `int` | `0` | Maximum duration in seconds to wait for the project to be running\|healthy |
<!---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

@@ -139,6 +139,17 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: insecure-registry
value_type: stringArray
default_value: '[]'
description: |
Use insecure registry to pull Compose OCI artifacts. Doesn't apply to images
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-ansi
value_type: bool
default_value: "false"
@@ -290,6 +301,57 @@ examples: |-
$ docker compose -f ~/sandbox/rails/compose.yaml pull db
```
#### Using an OCI published artifact
You can use the `-f` flag with the `oci://` prefix to reference a Compose file that has been published to an OCI registry.
This allows you to distribute and version your Compose configurations as OCI artifacts.
To use a Compose file from an OCI registry:
```console
$ docker compose -f oci://registry.example.com/my-compose-project:latest up
```
You can also combine OCI artifacts with local files:
```console
$ docker compose -f oci://registry.example.com/my-compose-project:v1.0 -f compose.override.yaml up
```
The OCI artifact must contain a valid Compose file. You can publish Compose files to an OCI registry using the
`docker compose publish` command.
#### Using a git repository
You can use the `-f` flag to reference a Compose file from a git repository. Compose supports various git URL formats:
Using HTTPS:
```console
$ docker compose -f https://github.com/user/repo.git up
```
Using SSH:
```console
$ docker compose -f git@github.com:user/repo.git up
```
You can specify a specific branch, tag, or commit:
```console
$ docker compose -f https://github.com/user/repo.git@main up
$ docker compose -f https://github.com/user/repo.git@v1.0.0 up
$ docker compose -f https://github.com/user/repo.git@abc123 up
```
You can also specify a subdirectory within the repository:
```console
$ docker compose -f https://github.com/user/repo.git#main:path/to/compose.yaml up
```
When using git resources, Compose will clone the repository and use the specified Compose file. You can combine
git resources with local files:
```console
$ docker compose -f https://github.com/user/repo.git -f compose.override.yaml up
```
### Use `-p` to specify a project name
Each configuration has a project name. Compose sets the project name using

View File

@@ -5,6 +5,26 @@ usage: docker compose alpha publish [OPTIONS] REPOSITORY[:TAG]
pname: docker compose alpha
plink: docker_compose_alpha.yaml
options:
- option: app
value_type: bool
default_value: "false"
description: Published compose application (includes referenced images)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: insecure-registry
value_type: bool
default_value: "false"
description: Use insecure registry
deprecated: false
hidden: true
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: oci-version
value_type: string
description: |

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,16 @@ 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"

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

@@ -5,6 +5,12 @@ long: |-
With this subcommand, you can run arbitrary commands in your services. Commands allocate a TTY by default, so
you can use a command such as `docker compose exec web sh` to get an interactive prompt.
By default, Compose will enter container in interactive mode and allocate a TTY, while the equivalent `docker exec`
command requires passing `--interactive --tty` flags to get the same behavior. Compose also support those two flags
to offer a smooth migration between commands, whenever they are no-op by default. Still, `interactive` can be used to
force disabling interactive mode (`--interactive=false`), typically when `docker compose exec` command is used inside
a script.
usage: docker compose exec [OPTIONS] SERVICE COMMAND [ARGS...]
pname: docker compose
plink: docker_compose.yaml
@@ -52,12 +58,12 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: no-TTY
- option: no-tty
shorthand: T
value_type: bool
default_value: "true"
description: |
Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.
Disable pseudo-TTY allocation. By default 'docker compose exec' allocates a TTY.
deprecated: false
hidden: false
experimental: false

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