Compare commits

...

75 Commits

Author SHA1 Message Date
Nicolas De Loof
ee33143026 capture git fetch output when debug output is enabled
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-04-08 12:03:41 +02:00
dependabot[bot]
cb0b5f6e27 build(deps): bump golang.org/x/sync from 0.12.0 to 0.13.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.12.0 to 0.13.0.
- [Commits](https://github.com/golang/sync/compare/v0.12.0...v0.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 11:51:03 +02:00
dependabot[bot]
1384853538 build(deps): bump golang.org/x/sys from 0.31.0 to 0.32.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/sys/compare/v0.31.0...v0.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 11:39:33 +02:00
Guillaume Lours
096b1e32d3 plugin provider support: check docker model runner status
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-04-08 11:19:01 +02:00
Guillaume Lours
bf71138df6 cleanup runPluging function
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-04-08 11:19:01 +02:00
sigi-glovebox
a1f673dcf5 Update secret detector to fix vulnerability https://github.com/golang-jwt/jwt/security/advisories/GHSA-mh63-6h87-95cp
Signed-off-by: sigi-glovebox <sigi@gloveboxapp.com>
2025-04-03 21:06:26 +02:00
Guillaume Lours
02c747a7de bump compose-go to custom version of v2.5.0
should be replace by v2.5.1 it will be released

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-04-03 15:13:44 +02:00
Nicolas De Loof
88f4f265db communicate with plugin using json events
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-04-03 15:13:44 +02:00
Nicolas De Loof
e67348222f DRAFT external services plugin support
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-04-03 15:13:44 +02:00
Suleiman Dibirov
b543380708 feat(run): Add --quiet and --quiet-build options for the run command
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-04-03 14:50:38 +02:00
Guillaume Lours
2e75185a07 bump golang to 1.23.8
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-04-02 16:31:43 +02:00
Guillaume Lours
7bedb5a02c bump golangci-lint to version v2.0.2
and apply migration script

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-31 18:50:15 +02:00
Nicolas De Loof
f9cd4d0b1d bump docker,cli,buildx
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-31 11:05:19 +02:00
Nicolas De Loof
0badcf3c8d include implicit build dependencies in build command
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-28 10:25:41 +01:00
k-kbk
ec49db98d4 fix: replace docker-compose.yml with compose.yaml
Signed-off-by: k-kbk <kkbk0077@gmail.com>
2025-03-27 15:44:00 +01:00
k-kbk
e5a353b34d fix: replace docker-compose.yml with compose.yaml
Signed-off-by: k-kbk <kkbk0077@gmail.com>
2025-03-27 15:44:00 +01:00
Nicolas De Loof
43e456145c fix scale completion
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-26 17:44:54 +01:00
Nicolas De Loof
75368c7859 introduce build --print to dump equivalent bakefile
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-26 12:24:31 +01:00
Suleiman Dibirov
6e814eac35 fix(secrets): Reverted secrets file mode 440 -> 444
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
2025-03-25 07:00:24 +01:00
Nicolas De Loof
a0d1c3f944 introduce config --no-env-resolution
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-24 15:46:04 +01:00
Nicolas De Loof
0c5bd16da1 bake parses "${}" in DockerfileInline as a variable
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-24 11:36:42 +01:00
Remco Kranenburg
b0badf1eb0 Set watch option --prune=true as default
Signed-off-by: Remco Kranenburg <remco.kranenburg@crunchr.com>
2025-03-19 17:48:05 +01:00
Nicolas De Loof
342a2a9e71 Fix support for depends_on.restart in up and restart commands
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-19 15:29:50 +01:00
dependabot[bot]
7814e5798c build(deps): bump github.com/containerd/containerd/v2
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.0.3 to 2.0.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.0.3...v2.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-18 11:02:40 +01:00
dependabot[bot]
42b2e11094 build(deps): bump github.com/docker/buildx from 0.21.2 to 0.21.3
Bumps [github.com/docker/buildx](https://github.com/docker/buildx) from 0.21.2 to 0.21.3.
- [Release notes](https://github.com/docker/buildx/releases)
- [Commits](https://github.com/docker/buildx/compare/v0.21.2...v0.21.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-18 10:45:25 +01:00
Nicolas De Loof
6a8c0988cf run only loads required service env_file and ignores others
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-18 09:39:15 +01:00
Matiboux
9129abe516 Fix edge-case bug path prefix check for watch & bind mounts
Signed-off-by: Matiboux <matiboux@gmail.com>
2025-03-17 17:40:40 +01:00
Nicolas De Loof
f38f3f754c PWD
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-17 17:26:45 +01:00
Nicolas De Loof
ea07ba8e2a fix support for secret set by env inside included file
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-14 16:21:45 +01:00
Max Proske
432ae23b0e Test commandName subcommand order
Signed-off-by: Max Proske <max@mproske.com>
2025-03-14 10:00:45 +01:00
Guillaume Lours
b6f313b8a5 bump compose-go to version v2.4.9
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-13 16:54:22 +01:00
Guillaume Lours
13618756dc make publish a regular command of Compose
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-13 16:17:00 +01:00
Dominik Menke
6c1e21572a lint: address gofumpt issues
Signed-off-by: Dominik Menke <dom@digineo.de>
2025-03-13 14:23:51 +01:00
Dominik Menke
33e863ac6c fix linting issue
Signed-off-by: Dominik Menke <dom@digineo.de>
2025-03-13 14:23:51 +01:00
Dominik Menke
f70209cf15 review: move Summary/Replica collection from cmd/ to pkg/
Signed-off-by: Dominik Menke <dom@digineo.de>
2025-03-13 14:23:51 +01:00
Dominik Menke
62e832eb50 compose top: reduce tabwriter padding
Signed-off-by: Dominik Menke <dom@digineo.de>
2025-03-13 14:23:51 +01:00
Dominik Menke
80e8fda14f compose top: ensure CMD is right-most column
Signed-off-by: Dominik Menke <dom@digineo.de>
2025-03-13 14:23:51 +01:00
Dominik Menke
375a279785 top: expose container labels
Signed-off-by: Dominik Menke <dom@digineo.de>
2025-03-13 14:23:51 +01:00
Dominik Menke
a766e1669a condense output of compose top
This changes the output format of `compose top` and inlines the service
container name into the table.

Previously, `compose top` had printed something like:

  <service name>
  UID    PID   ...
  root   1     ...

Now, the output looks more like this:

  SERVICE   UID    PID   ...
  <name>    root   1     ...

Signed-off-by: Dominik Menke <dom@digineo.de>
2025-03-13 14:23:51 +01:00
Matt Landis
793c6f1715 add cli.isatty attribute to spans generated by compose
Signed-off-by: Matt Landis <matt.landis@docker.com>
2025-03-13 09:06:15 +01:00
dependabot[bot]
8e3e1f7f8b 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 0.8.1 to 1.0.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/v0.8.1...v1.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-12 15:50:14 +01:00
Nicolas De Loof
83cafe2838 Add support to pass env-from-file to docker compose run
Signed-off-by: Vedant Koditkar <vedant.koditkar@outlook.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-12 09:04:39 +01:00
Guillaume Lours
55b5f233c2 use Defang secret-detector to identify potential secret leaks before publishing OCI artifacts
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-11 15:02:37 +01:00
Nicolas De Loof
c3a0c35681 implement extends.file replace without yqlib
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-11 14:18:41 +01:00
Nicolas De Loof
8615e9a7c1 deprecate --y, prefer --yes
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-11 09:01:36 +01:00
Nicolas De Loof
b23728941d only load env_file after services have been selected
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-10 08:32:03 +01:00
dependabot[bot]
1a7343bc88 build(deps): bump github.com/moby/buildkit from 0.20.0 to 0.20.1
Bumps [github.com/moby/buildkit](https://github.com/moby/buildkit) from 0.20.0 to 0.20.1.
- [Release notes](https://github.com/moby/buildkit/releases)
- [Commits](https://github.com/moby/buildkit/compare/v0.20.0...v0.20.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 16:44:02 +01:00
Guillaume Lours
41e6094041 add warning message when a remote configuration include an another remote config
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-07 16:30:32 +01:00
Nicolas De Loof
66a47169d5 Publish compose file with required siblings used by extends
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-07 13:58:10 +01:00
dependabot[bot]
4c72d3a0e3 build(deps): bump golang.org/x/sys from 0.30.0 to 0.31.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.30.0 to 0.31.0.
- [Commits](https://github.com/golang/sys/compare/v0.30.0...v0.31.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 19:07:25 +01:00
dependabot[bot]
59f39b9990 build(deps): bump google.golang.org/grpc from 1.70.0 to 1.71.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.70.0 to 1.71.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.70.0...v1.71.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 18:55:59 +01:00
dependabot[bot]
7ab65ba127 build(deps): bump golang.org/x/sync from 0.11.0 to 0.12.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.11.0 to 0.12.0.
- [Commits](https://github.com/golang/sync/compare/v0.11.0...v0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 18:44:23 +01:00
Guillaume Lours
d9f05d72d2 improve message suggesting using bake
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-06 18:12:43 +01:00
Guillaume Lours
7b88c5b0ed display interpolation variables and their values when running a remote stack
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-06 09:46:37 +01:00
dependabot[bot]
eaf9800948 build(deps): bump github.com/opencontainers/image-spec
Bumps [github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/opencontainers/image-spec/releases)
- [Changelog](https://github.com/opencontainers/image-spec/blob/main/RELEASES.md)
- [Commits](https://github.com/opencontainers/image-spec/compare/v1.1.0...v1.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 10:19:45 +01:00
Nicolas De Loof
4c2ecb542f reject compose file with bind mounts
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-04 16:10:53 +01:00
Nicolas De Loof
bcd000ab40 refuse to publish compose file with local include
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-04 15:55:17 +01:00
dependabot[bot]
8092ce9414 build(deps): bump github.com/docker/buildx from 0.21.1 to 0.21.2
Bumps [github.com/docker/buildx](https://github.com/docker/buildx) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/docker/buildx/releases)
- [Commits](https://github.com/docker/buildx/compare/v0.21.1...v0.21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 10:44:04 +01:00
dependabot[bot]
97595066e3 build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.0.0+incompatible to 28.0.1+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.0.0...v28.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 07:52:24 +01:00
dependabot[bot]
508309414f build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.0.0+incompatible to 28.0.1+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.0.0...v28.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 07:52:00 +01:00
Guillaume Lours
b6c8a2b9fc display the location of OCI or GIT Compose stack download
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-03 22:09:06 +01:00
Nicolas De Loof
19571c2c81 e2e test for watch.include
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-03 21:59:26 +01:00
Nicolas De Loof
0ef7bbcddc introduce watch.include
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-03-03 21:59:26 +01:00
Guillaume Lours
66dfa7d181 block the publication of an OCI artifact if one or more services contain only a build section
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-03-03 16:23:21 +01:00
Max Proske
876ecc48be Test version command
Signed-off-by: Max Proske <max@mproske.com>
2025-02-26 16:35:08 +01:00
Nicolas De Loof
c7bf302c23 wrap builder execution within a project/build span
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-02-26 11:23:01 +01:00
Nicolas De Loof
7b3bdbe037 otel attribute to track builder implementation selected
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-02-26 11:23:01 +01:00
dependabot[bot]
094b48fd74 build(deps): bump github.com/google/go-cmp from 0.6.0 to 0.7.0
Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/google/go-cmp/releases)
- [Commits](https://github.com/google/go-cmp/compare/v0.6.0...v0.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-26 09:31:01 +01:00
dependabot[bot]
43c52e2a80 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 0.8.0 to 0.8.1.
- [Release notes](https://github.com/cncf-tags/container-device-interface/releases)
- [Commits](https://github.com/cncf-tags/container-device-interface/compare/v0.8.0...v0.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-26 09:30:52 +01:00
Nicolas De Loof
6c1ee1069b support refresh pull policy
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-02-25 17:05:23 +01:00
Nicolas De Loof
e38b729a30 fix service: additional_contexts running internal buildkit client
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-02-25 09:47:02 +01:00
Andrew Kramer
145bb8466d Update yaml docs
Signed-off-by: Andrew Kramer <andrew.d.kramer.80@gmail.com>
2025-02-22 11:17:19 +01:00
Andrew Kramer
acac184135 Link to configuration file docs
Signed-off-by: Andrew Kramer <andrew.d.kramer.80@gmail.com>
2025-02-22 11:17:19 +01:00
Simon Ser
3292740c19 build: only print COMPOSE_BAKE recommendation when disabled
docker-compose now prints a recommendation to set COMPOSE_BAKE=true
when service deps are used. However, when the user opts-in to bake,
the recommendation is still printed.

Only print the recommendation when bake is disabled.

Signed-off-by: Simon Ser <contact@emersion.fr>
2025-02-22 11:14:36 +01:00
Nicolas De Loof
cae8e84636 require go 1.23|1.24 (stable)
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-02-21 16:54:16 +01:00
101 changed files with 2775 additions and 351 deletions

View File

@ -1,9 +1,8 @@
version: "2"
run:
concurrency: 2
timeout: 10m
linters:
enable-all: false
disable-all: true
default: none
enable:
- copyloopvar
- depguard
@ -11,73 +10,74 @@ linters:
- errorlint
- gocritic
- gocyclo
- gofumpt
- goimports
- gomodguard
- revive
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- nolintlint
- revive
- staticcheck
- testifylint
- typecheck
- unconvert
- unparam
- unused
linters-settings:
revive:
rules:
- name: package-comments
disabled: true
depguard:
rules:
all:
deny:
- pkg: io/ioutil
desc: 'io/ioutil package has been deprecated'
- pkg: gopkg.in/yaml.v2
desc: 'compose-go uses yaml.v3'
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
- fmt
versions:
- github.com/distribution/distribution:
reason: "use distribution/reference"
- gotest.tools:
version: "< 3.0.0"
reason: "deprecated, pre-modules version"
gocritic:
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
enabled-tags:
- diagnostic
- opinionated
- style
disabled-checks:
- paramTypeCombine
- unnamedResult
- whyNoLint
gocyclo:
min-complexity: 16
lll:
line-length: 200
settings:
depguard:
rules:
all:
deny:
- pkg: io/ioutil
desc: io/ioutil package has been deprecated
- pkg: gopkg.in/yaml.v2
desc: compose-go uses yaml.v3
gocritic:
disabled-checks:
- paramTypeCombine
- unnamedResult
- whyNoLint
enabled-tags:
- diagnostic
- opinionated
- style
gocyclo:
min-complexity: 16
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
- fmt
versions:
- github.com/distribution/distribution:
reason: use distribution/reference
- gotest.tools:
version: < 3.0.0
reason: deprecated, pre-modules version
lll:
line-length: 200
revive:
rules:
- name: package-comments
disabled: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
issues:
# golangci hides some golint warnings (the warning about exported things
# without documentation for example), this will make it show them anyway.
exclude-use-default: false
# Maximum issues count per one linter.
# Set to 0 to disable.
# Default: 50
max-issues-per-linter: 0
# Maximum count of issues with the same text.
# Set to 0 to disable.
# Default: 3
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@ -15,9 +15,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
ARG GO_VERSION=1.22.10
ARG GO_VERSION=1.23.8
ARG XX_VERSION=1.6.1
ARG GOLANGCI_LINT_VERSION=v1.63.4
ARG GOLANGCI_LINT_VERSION=v2.0.2
ARG ADDLICENSE_VERSION=v1.0.0
ARG BUILD_TAGS="e2e"

View File

@ -56,6 +56,7 @@ func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
"cli/"+strings.Join(commandName(cmd), "-"),
)
cmdSpan.SetAttributes(attribute.StringSlice("cli.flags", getFlags(cmd.Flags())))
cmdSpan.SetAttributes(attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()))
cmd.SetContext(ctx)
wrapRunE(cmd, cmdSpan, tracingShutdown)
@ -114,13 +115,14 @@ func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.Shut
}
}
// commandName returns the path components for a given command.
// commandName returns the path components for a given command,
// in reverse alphabetical order for consistent usage metrics.
//
// The root Compose command and anything before (i.e. "docker")
// are not included.
//
// For example:
// - docker compose alpha watch -> [alpha, watch]
// - docker compose alpha watch -> [watch, alpha]
// - docker-compose up -> [up]
func commandName(cmd *cobra.Command) []string {
var name []string

View File

@ -20,6 +20,8 @@ import (
"reflect"
"testing"
commands "github.com/docker/compose/v2/cmd/compose"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
)
@ -61,3 +63,50 @@ func TestGetFlags(t *testing.T) {
})
}
}
func TestCommandName(t *testing.T) {
tests := []struct {
name string
setupCmd func() *cobra.Command
want []string
}{
{
name: "docker compose alpha watch -> [watch, alpha]",
setupCmd: func() *cobra.Command {
dockerCmd := &cobra.Command{Use: "docker"}
composeCmd := &cobra.Command{Use: commands.PluginName}
alphaCmd := &cobra.Command{Use: "alpha"}
watchCmd := &cobra.Command{Use: "watch"}
dockerCmd.AddCommand(composeCmd)
composeCmd.AddCommand(alphaCmd)
alphaCmd.AddCommand(watchCmd)
return watchCmd
},
want: []string{"watch", "alpha"},
},
{
name: "docker-compose up -> [up]",
setupCmd: func() *cobra.Command {
dockerComposeCmd := &cobra.Command{Use: commands.PluginName}
upCmd := &cobra.Command{Use: "up"}
dockerComposeCmd.AddCommand(upCmd)
return upCmd
},
want: []string{"up"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.setupCmd()
got := commandName(cmd)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("commandName() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -27,6 +27,7 @@ import (
"github.com/docker/cli/cli/command"
cliopts "github.com/docker/cli/opts"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
buildkit "github.com/moby/buildkit/util/progress/progressui"
"github.com/spf13/cobra"
@ -44,6 +45,7 @@ type buildOptions struct {
ssh string
builder string
deps bool
print bool
}
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
@ -76,6 +78,8 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
Quiet: opts.quiet,
Services: services,
Deps: opts.deps,
Memory: int64(opts.memory),
Print: opts.print,
SSHs: SSHKeys,
Builder: builderName,
}, nil
@ -131,6 +135,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
flags.VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.")
flags.StringVar(&p.Progress, "progress", string(buildkit.AutoMode), fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
flags.MarkHidden("progress") //nolint:errcheck
flags.BoolVar(&opts.print, "print", false, "Print equivalent bake file")
return cmd
}
@ -141,6 +146,8 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o
return err
}
services = addBuildDependencies(services, project)
if err := applyPlatforms(project, false); err != nil {
return err
}
@ -150,6 +157,23 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o
return err
}
apiBuildOptions.Memory = int64(opts.memory)
return backend.Build(ctx, project, apiBuildOptions)
}
func addBuildDependencies(services []string, project *types.Project) []string {
servicesWithDependencies := utils.NewSet(services...)
for _, service := range services {
build := project.Services[service].Build
if build != nil {
for _, target := range build.AdditionalContexts {
if s, found := strings.CutPrefix(target, types.ServicePrefix); found {
servicesWithDependencies.Add(s)
}
}
}
}
if len(servicesWithDependencies) > len(services) {
return addBuildDependencies(servicesWithDependencies.Elements(), project)
}
return servicesWithDependencies.Elements()
}

57
cmd/compose/build_test.go Normal file
View File

@ -0,0 +1,57 @@
/*
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 (
"slices"
"testing"
"github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
)
func Test_addBuildDependencies(t *testing.T) {
project := &types.Project{Services: types.Services{
"test": types.ServiceConfig{
Build: &types.BuildConfig{
AdditionalContexts: map[string]string{
"foo": "service:foo",
"bar": "service:bar",
},
},
},
"foo": types.ServiceConfig{
Build: &types.BuildConfig{
AdditionalContexts: map[string]string{
"zot": "service:zot",
},
},
},
"bar": types.ServiceConfig{
Build: &types.BuildConfig{},
},
"zot": types.ServiceConfig{
Build: &types.BuildConfig{},
},
}}
services := addBuildDependencies([]string{"test"}, project)
expected := []string{"test", "foo", "bar", "zot"}
slices.Sort(services)
slices.Sort(expected)
assert.DeepEqual(t, services, expected)
}

View File

@ -90,3 +90,13 @@ func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn
return values, cobra.ShellCompDirectiveNoFileComp
}
}
func completeScaleArgs(cli command.Cli, p *ProjectOptions) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
completions, directive := completeServiceNames(cli, p)(cmd, args, toComplete)
for i, completion := range completions {
completions[i] = completion + "="
}
return completions, directive
}
}

View File

@ -72,17 +72,16 @@ const (
)
// rawEnv load a dot env file using docker/cli key=value parser, without attempt to interpolate or evaluate values
func rawEnv(r io.Reader, filename string, lookup func(key string) (string, bool)) (map[string]string, error) {
func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error {
lines, err := kvfile.ParseFromReader(r, lookup)
if err != nil {
return nil, fmt.Errorf("failed to parse env_file %s: %w", filename, err)
return fmt.Errorf("failed to parse env_file %s: %w", filename, err)
}
vars := types.Mapping{}
for _, line := range lines {
key, value, _ := strings.Cut(line, "=")
vars[key] = value
}
return vars, nil
return nil
}
func init() {
@ -170,7 +169,7 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF
return Adapt(func(ctx context.Context, args []string) error {
options := []cli.ProjectOptionsFn{
cli.WithResolvedPaths(true),
cli.WithDiscardEnvFile,
cli.WithoutEnvironmentResolution,
}
project, metrics, err := o.ToProject(ctx, dockerCli, args, options...)
@ -180,6 +179,11 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF
ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics)
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return err
}
return fn(ctx, project, args)
})
}
@ -372,17 +376,24 @@ func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceL
if o.Offline {
return nil
}
git := remote.NewGitRemoteLoader(o.Offline)
git := remote.NewGitRemoteLoader(dockerCli, o.Offline)
oci := remote.NewOCIRemoteLoader(dockerCli, o.Offline)
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
}
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
@ -538,10 +549,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
}
composeCmd := cmd
for {
if composeCmd.Name() == PluginName {
break
}
for composeCmd.Name() != PluginName {
if !composeCmd.HasParent() {
return fmt.Errorf("error parsing command line, expected %q", PluginName)
}
@ -625,6 +633,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
scaleCommand(&opts, dockerCli, backend),
statsCommand(&opts, dockerCli),
watchCommand(&opts, dockerCli, backend),
publishCommand(&opts, dockerCli, backend),
alphaCommand(&opts, dockerCli, backend),
)

View File

@ -47,6 +47,7 @@ type configOptions struct {
noInterpolate bool
noNormalize bool
noResolvePath bool
noResolveEnv bool
services bool
volumes bool
profiles bool
@ -135,6 +136,7 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model")
flags.BoolVar(&opts.noResolvePath, "no-path-resolution", false, "Don't resolve file paths")
flags.BoolVar(&opts.noConsistency, "no-consistency", false, "Don't check model consistency - warning: may produce invalid Compose output")
flags.BoolVar(&opts.noResolveEnv, "no-env-resolution", false, "Don't resolve service env files")
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.")
@ -190,6 +192,13 @@ func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts confi
}
}
if !opts.noResolveEnv {
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return nil, err
}
}
if !opts.noConsistency {
err := project.CheckContainerNameUnicity()
if err != nil {

View File

@ -26,7 +26,9 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/pkg/api"
)
@ -81,7 +83,15 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
flags.BoolVarP(&opts.AssumeYes, "y", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
flags.BoolVarP(&opts.AssumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
return cmd
}

View File

@ -86,7 +86,7 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, op
if err != nil {
return err
}
projectOptions, err := opts.composeOptions.toProjectOptions()
projectOptions, err := opts.composeOptions.toProjectOptions() //nolint:staticcheck
if err != nil {
return err
}

View File

@ -17,9 +17,21 @@
package compose
import (
"context"
"fmt"
"io"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/compose-spec/compose-go/v2/cli"
"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/v2/pkg/utils"
)
@ -72,3 +84,208 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
}
return nil
}
// isRemoteConfig checks if the main compose file is from a remote source (OCI or Git)
func isRemoteConfig(dockerCli command.Cli, options buildOptions) bool {
if len(options.ConfigPaths) == 0 {
return false
}
remoteLoaders := options.remoteLoaders(dockerCli)
for _, loader := range remoteLoaders {
if loader.Accept(options.ConfigPaths[0]) {
return true
}
}
return false
}
// checksForRemoteStack handles environment variable prompts for remote configurations
func checksForRemoteStack(ctx context.Context, dockerCli command.Cli, project *types.Project, options buildOptions, assumeYes bool, cmdEnvs []string) error {
if !isRemoteConfig(dockerCli, options) {
return nil
}
if metrics, ok := ctx.Value(tracing.MetricsKey{}).(tracing.Metrics); ok && metrics.CountIncludesRemote > 0 {
if err := confirmRemoteIncludes(dockerCli, options, assumeYes); err != nil {
return err
}
}
displayLocationRemoteStack(dockerCli, project, options)
return promptForInterpolatedVariables(ctx, dockerCli, options.ProjectOptions, assumeYes, cmdEnvs)
}
// Prepare the values map and collect all variables info
type varInfo struct {
name string
value string
source string
required bool
defaultValue string
}
// promptForInterpolatedVariables displays all variables and their values at once,
// then prompts for confirmation
func promptForInterpolatedVariables(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, assumeYes bool, cmdEnvs []string) error {
if assumeYes {
return nil
}
varsInfo, noVariables, err := extractInterpolationVariablesFromModel(ctx, dockerCli, projectOptions, cmdEnvs)
if err != nil {
return err
}
if noVariables {
return nil
}
displayInterpolationVariables(dockerCli.Out(), varsInfo)
// Prompt for confirmation
userInput := prompt.NewPrompt(dockerCli.In(), dockerCli.Out())
msg := "\nDo you want to proceed with these variables? [Y/n]: "
confirmed, err := userInput.Confirm(msg, true)
if err != nil {
return err
}
if !confirmed {
return fmt.Errorf("operation cancelled by user")
}
return nil
}
func extractInterpolationVariablesFromModel(ctx context.Context, dockerCli command.Cli, projectOptions *ProjectOptions, cmdEnvs []string) ([]varInfo, bool, error) {
cmdEnvMap := extractEnvCLIDefined(cmdEnvs)
// Create a model without interpolation to extract variables
opts := configOptions{
noInterpolate: true,
ProjectOptions: projectOptions,
}
model, err := opts.ToModel(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
if err != nil {
return nil, false, err
}
// Extract variables that need interpolation
variables := template.ExtractVariables(model, template.DefaultPattern)
if len(variables) == 0 {
return nil, true, nil
}
var varsInfo []varInfo
proposedValues := make(map[string]string)
for name, variable := range variables {
info := varInfo{
name: name,
required: variable.Required,
defaultValue: variable.DefaultValue,
}
// Determine value and source based on priority
if value, exists := cmdEnvMap[name]; exists {
info.value = value
info.source = "command-line"
proposedValues[name] = value
} else if value, exists := os.LookupEnv(name); exists {
info.value = value
info.source = "environment"
proposedValues[name] = value
} else if variable.DefaultValue != "" {
info.value = variable.DefaultValue
info.source = "compose file"
proposedValues[name] = variable.DefaultValue
} else {
info.value = "<unset>"
info.source = "none"
}
varsInfo = append(varsInfo, info)
}
return varsInfo, false, nil
}
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]
}
}
return cmdEnvMap
}
func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) {
// Display all variables in a table format
_, _ = fmt.Fprintln(writer, "\nFound the following variables in configuration:")
w := tabwriter.NewWriter(writer, 0, 0, 3, ' ', 0)
_, _ = fmt.Fprintln(w, "VARIABLE\tVALUE\tSOURCE\tREQUIRED\tDEFAULT")
sort.Slice(varsInfo, func(a, b int) bool {
return varsInfo[a].name < varsInfo[b].name
})
for _, info := range varsInfo {
required := "no"
if info.required {
required = "yes"
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
info.name,
info.value,
info.source,
required,
info.defaultValue,
)
}
_ = w.Flush()
}
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 {
_, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
}
}
func confirmRemoteIncludes(dockerCli command.Cli, options buildOptions, assumeYes bool) error {
if assumeYes {
return nil
}
var remoteIncludes []string
remoteLoaders := options.ProjectOptions.remoteLoaders(dockerCli) //nolint:staticcheck
for _, cf := range options.ProjectOptions.ConfigPaths { //nolint:staticcheck
for _, loader := range remoteLoaders {
if loader.Accept(cf) {
remoteIncludes = append(remoteIncludes, cf)
break
}
}
}
if len(remoteIncludes) == 0 {
return nil
}
_, _ = fmt.Fprintln(dockerCli.Out(), "\nWarning: This Compose project includes files from remote sources:")
for _, include := range remoteIncludes {
_, _ = fmt.Fprintf(dockerCli.Out(), " - %s\n", include)
}
_, _ = fmt.Fprintln(dockerCli.Out(), "\nRemote includes could potentially be malicious. Make sure you trust the source.")
msg := "Do you want to continue? [y/N]: "
confirmed, err := prompt.NewPrompt(dockerCli.In(), dockerCli.Out()).Confirm(msg, false)
if err != nil {
return err
}
if !confirmed {
return fmt.Errorf("operation cancelled by user")
}
return nil
}

View File

@ -17,10 +17,20 @@
package compose
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"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"
)
func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
@ -128,3 +138,257 @@ func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
})
}
func TestIsRemoteConfig(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
tests := []struct {
name string
configPaths []string
want bool
}{
{
name: "empty config paths",
configPaths: []string{},
want: false,
},
{
name: "local file",
configPaths: []string{"docker-compose.yaml"},
want: false,
},
{
name: "OCI reference",
configPaths: []string{"oci://registry.example.com/stack:latest"},
want: true,
},
{
name: "GIT reference",
configPaths: []string{"git://github.com/user/repo.git"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: tt.configPaths,
},
}
got := isRemoteConfig(cli, opts)
require.Equal(t, tt.want, got)
})
}
}
func TestDisplayLocationRemoteStack(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
buf := new(bytes.Buffer)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
project := &types.Project{
Name: "test-project",
WorkingDir: "/tmp/test",
}
options := buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{"oci://registry.example.com/stack:latest"},
},
}
displayLocationRemoteStack(cli, project, options)
output := buf.String()
require.Equal(t, output, fmt.Sprintf("Your compose stack %q is stored in %q\n", "oci://registry.example.com/stack:latest", "/tmp/test"))
}
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) }()
// Create a temporary compose file
composeContent := `
services:
app:
image: nginx
environment:
- TEST_VAR=${TEST_VAR:?required} # required with default
- API_KEY=${API_KEY:?} # required without default
- DEBUG=${DEBUG:-true} # optional with default
- UNSET_VAR # optional without default
`
composePath := filepath.Join(tmpDir, "docker-compose.yml")
err = os.WriteFile(composePath, []byte(composeContent), 0o644)
require.NoError(t, err)
buf := new(bytes.Buffer)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
// Create ProjectOptions with the temporary compose file
projectOptions := &ProjectOptions{
ConfigPaths: []string{composePath},
}
// 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")
}()
// Extract variables from the model
info, noVariables, err := extractInterpolationVariablesFromModel(ctx, cli, projectOptions, []string{})
require.NoError(t, err)
require.False(t, noVariables)
// Display the variables
displayInterpolationVariables(cli.Out(), info)
// Expected output format with proper spacing
expected := "\nFound the following variables in configuration:\n" +
"VARIABLE VALUE SOURCE REQUIRED DEFAULT\n" +
"API_KEY 123456 environment yes \n" +
"DEBUG true compose file no true\n" +
"TEST_VAR test-value environment yes \n"
// Normalize spaces and newlines for comparison
normalizeSpaces := func(s string) string {
// Replace multiple spaces with a single space
s = strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
return s
}
actualOutput := buf.String()
// Compare normalized strings
require.Equal(t,
normalizeSpaces(expected),
normalizeSpaces(actualOutput),
"\nExpected:\n%s\nGot:\n%s", expected, actualOutput)
}
func TestConfirmRemoteIncludes(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cli := mocks.NewMockCli(ctrl)
tests := []struct {
name string
opts buildOptions
assumeYes bool
userInput string
wantErr bool
errMessage string
wantPrompt bool
wantOutput string
}{
{
name: "no remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"docker-compose.yaml",
"./local/path/compose.yaml",
},
},
},
assumeYes: false,
wantErr: false,
wantPrompt: false,
},
{
name: "assume yes with remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
"git://github.com/user/repo.git",
},
},
},
assumeYes: true,
wantErr: false,
wantPrompt: false,
},
{
name: "user confirms remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
"git://github.com/user/repo.git",
},
},
},
assumeYes: false,
userInput: "y\n",
wantErr: false,
wantPrompt: true,
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
" - oci://registry.example.com/stack:latest\n" +
" - git://github.com/user/repo.git\n" +
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
"Do you want to continue? [y/N]: ",
},
{
name: "user rejects remote includes",
opts: buildOptions{
ProjectOptions: &ProjectOptions{
ConfigPaths: []string{
"oci://registry.example.com/stack:latest",
},
},
},
assumeYes: false,
userInput: "n\n",
wantErr: true,
errMessage: "operation cancelled by user",
wantPrompt: true,
wantOutput: "\nWarning: This Compose project includes files from remote sources:\n" +
" - oci://registry.example.com/stack:latest\n" +
"\nRemote includes could potentially be malicious. Make sure you trust the source.\n" +
"Do you want to continue? [y/N]: ",
},
}
buf := new(bytes.Buffer)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
if tt.wantPrompt {
inbuf := io.NopCloser(bytes.NewBufferString(tt.userInput))
cli.EXPECT().In().Return(streams.NewIn(inbuf)).AnyTimes()
}
err := confirmRemoteIncludes(cli, tt.opts, tt.assumeYes)
if tt.wantErr {
require.Error(t, err)
require.Equal(t, tt.errMessage, err.Error())
} else {
require.NoError(t, err)
}
if tt.wantOutput != "" {
require.Equal(t, tt.wantOutput, buf.String())
}
buf.Reset()
})
}
}

View File

@ -18,11 +18,14 @@ package compose
import (
"context"
"errors"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type publishOptions struct {
@ -43,23 +46,35 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic
RunE: Adapt(func(ctx context.Context, args []string) error {
return runPublish(ctx, dockerCli, backend, opts, args[0])
}),
Args: cobra.ExactArgs(1),
Args: cli.ExactArgs(1),
}
flags := cmd.Flags()
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
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, "y", "y", false, `Assume "yes" as answer to all prompts`)
flags.BoolVarP(&opts.assumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
return cmd
}
func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error {
project, _, err := opts.ToProject(ctx, dockerCli, nil)
project, metrics, err := opts.ToProject(ctx, dockerCli, nil)
if err != nil {
return err
}
if metrics.CountIncludesLocal > 0 {
return errors.New("cannot publish compose file with local includes")
}
return backend.Publish(ctx, project, repository, api.PublishOptions{
ResolveImageDigests: opts.resolveImageDigests,
OCIVersion: api.OCIVersion(opts.ociVersion),

View File

@ -19,8 +19,10 @@ package compose
import (
"context"
"fmt"
"os"
"strings"
"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"
@ -44,6 +46,7 @@ type runOptions struct {
Service string
Command []string
environment []string
envFiles []string
Detach bool
Remove bool
noTty bool
@ -64,6 +67,7 @@ type runOptions struct {
noDeps bool
ignoreOrphans bool
removeOrphans bool
quiet bool
quietPull bool
}
@ -116,6 +120,29 @@ 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()
for _, file := range options.envFiles {
f, err := os.Open(file)
if err != nil {
return nil, err
}
vars, err := dotenv.ParseWithLookup(f, func(k string) (string, bool) {
value, ok := environment[k]
return value, ok
})
if err != nil {
return nil, nil
}
for k, v := range vars {
if _, ok := environment[k]; !ok {
environment[k] = v
}
}
}
return environment, nil
}
func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
options := runOptions{
composeOptions: &composeOptions{
@ -154,11 +181,24 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
options.noTty = !options.tty
}
}
if options.quiet {
progress.Mode = progress.ModeQuiet
devnull, err := os.Open(os.DevNull)
if err != nil {
return err
}
os.Stdout = devnull
}
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.WithDiscardEnvFile)
project, _, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithoutEnvironmentResolution)
if err != nil {
return err
}
project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return err
}
@ -175,6 +215,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
flags := cmd.Flags()
flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID")
flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables")
flags.StringArrayVar(&options.envFiles, "env-from-file", []string{}, "Set environment variables from file")
flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label")
flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits")
flags.BoolVarP(&options.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected)")
@ -190,6 +231,8 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to")
flags.BoolVarP(&options.servicePorts, "service-ports", "P", false, "Run command with all service's ports enabled and mapped to the host")
flags.StringVar(&createOpts.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`)
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
flags.BoolVar(&buildOpts.quiet, "quiet-build", false, "Suppress progress output from the build process")
flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information")
flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container")
flags.BoolVar(&options.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
@ -224,6 +267,10 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
return err
}
if err := checksForRemoteStack(ctx, dockerCli, project, buildOpts, createOpts.AssumeYes, []string{}); err != nil {
return err
}
err = progress.Run(ctx, func(ctx context.Context) error {
var buildForDeps *api.BuildOptions
if !createOpts.noBuild {
@ -260,6 +307,11 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
buildForRun = &bo
}
environment, err := options.getEnvironment()
if err != nil {
return err
}
// start container and attach to container streams
runOpts := api.RunOptions{
Build: buildForRun,
@ -274,7 +326,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
User: options.user,
CapAdd: options.capAdd.GetAll(),
CapDrop: options.capDrop.GetAll(),
Environment: options.environment,
Environment: environment.Values(),
Entrypoint: options.entrypointCmd,
Labels: labels,
UseNetworkAliases: options.useAliases,

View File

@ -51,7 +51,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
}
return runScale(ctx, dockerCli, backend, opts, serviceTuples)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
ValidArgsFunction: completeScaleArgs(dockerCli, p),
}
flags := scaleCmd.Flags()
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services")

View File

@ -49,6 +49,11 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
return topCmd
}
type (
topHeader map[string]int // maps a proc title to its output index
topEntries map[string]string
)
func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
@ -63,30 +68,76 @@ func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opt
return containers[i].Name < containers[j].Name
})
for _, container := range containers {
_, _ = fmt.Fprintf(dockerCli.Out(), "%s\n", container.Name)
err := psPrinter(dockerCli.Out(), func(w io.Writer) {
for _, proc := range container.Processes {
info := []interface{}{}
for _, p := range proc {
info = append(info, p)
}
_, _ = fmt.Fprintf(w, strings.Repeat("%s\t", len(info))+"\n", info...)
header, entries := collectTop(containers)
return topPrint(dockerCli.Out(), header, entries)
}
func collectTop(containers []api.ContainerProcSummary) (topHeader, []topEntries) {
// map column name to its header (should keep working if backend.Top returns
// varying columns for different containers)
header := topHeader{"SERVICE": 0, "#": 1}
// assume one process per container and grow if needed
entries := make([]topEntries, 0, len(containers))
for _, container := range containers {
for _, proc := range container.Processes {
entry := topEntries{
"SERVICE": container.Service,
"#": container.Replica,
}
_, _ = fmt.Fprintln(w)
},
container.Titles...)
if err != nil {
return err
for i, title := range container.Titles {
if _, exists := header[title]; !exists {
header[title] = len(header)
}
entry[title] = proc[i]
}
entries = append(entries, entry)
}
}
return nil
// ensure CMD is the right-most column
if pos, ok := header["CMD"]; ok {
maxPos := pos
for h, i := range header {
if i > maxPos {
maxPos = i
}
if i > pos {
header[h] = i - 1
}
}
header["CMD"] = maxPos
}
return header, entries
}
func psPrinter(out io.Writer, printer func(writer io.Writer), headers ...string) error {
w := tabwriter.NewWriter(out, 5, 1, 3, ' ', 0)
_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
printer(w)
func topPrint(out io.Writer, headers topHeader, rows []topEntries) error {
if len(rows) == 0 {
return nil
}
w := tabwriter.NewWriter(out, 4, 1, 2, ' ', 0)
// write headers in the order we've encountered them
h := make([]string, len(headers))
for title, index := range headers {
h[index] = title
}
_, _ = fmt.Fprintln(w, strings.Join(h, "\t"))
for _, row := range rows {
// write proc data in header order
r := make([]string, len(headers))
for title, index := range headers {
if v, ok := row[title]; ok {
r[index] = v
} else {
r[index] = "-"
}
}
_, _ = fmt.Fprintln(w, strings.Join(r, "\t"))
}
return w.Flush()
}

329
cmd/compose/top_test.go Normal file
View File

@ -0,0 +1,329 @@
/*
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 compose
import (
"bytes"
"strings"
"testing"
"github.com/docker/compose/v2/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var topTestCases = []struct {
name string
titles []string
procs [][]string
header topHeader
entries []topEntries
output string
}{
{
name: "noprocs",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{},
header: topHeader{"SERVICE": 0, "#": 1},
entries: []topEntries{},
output: "",
},
{
name: "simple",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "1", "0", "12:00", "?", "00:00:01", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"CMD": 9,
},
entries: []topEntries{
{
"SERVICE": "simple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:01",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID PID PPID C STIME TTY TIME CMD
simple 1 root 1 1 0 12:00 ? 00:00:01 /entrypoint
`),
},
{
name: "noppid",
titles: []string{"UID", "PID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "0", "12:00", "?", "00:00:02", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"C": 4,
"STIME": 5,
"TTY": 6,
"TIME": 7,
"CMD": 8,
},
entries: []topEntries{
{
"SERVICE": "noppid",
"#": "1",
"UID": "root",
"PID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:02",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID PID C STIME TTY TIME CMD
noppid 1 root 1 0 12:00 ? 00:00:02 /entrypoint
`),
},
{
name: "extra-hdr",
titles: []string{"UID", "GID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "1", "1", "0", "12:00", "?", "00:00:03", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"GID": 3,
"PID": 4,
"PPID": 5,
"C": 6,
"STIME": 7,
"TTY": 8,
"TIME": 9,
"CMD": 10,
},
entries: []topEntries{
{
"SERVICE": "extra-hdr",
"#": "1",
"UID": "root",
"GID": "1",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:03",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE # UID GID PID PPID C STIME TTY TIME CMD
extra-hdr 1 root 1 1 1 0 12:00 ? 00:00:03 /entrypoint
`),
},
{
name: "multiple",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{
{"root", "1", "1", "0", "12:00", "?", "00:00:04", "/entrypoint"},
{"root", "123", "1", "0", "12:00", "?", "00:00:42", "sleep infinity"},
},
header: topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"CMD": 9,
},
entries: []topEntries{
{
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:04",
"CMD": "/entrypoint",
},
{
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "123",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:42",
"CMD": "sleep infinity",
},
},
output: trim(`
SERVICE # UID PID PPID C STIME TTY TIME CMD
multiple 1 root 1 1 0 12:00 ? 00:00:04 /entrypoint
multiple 1 root 123 1 0 12:00 ? 00:00:42 sleep infinity
`),
},
}
// TestRunTopCore only tests the core functionality of runTop: formatting
// and printing of the output of (api.Service).Top().
func TestRunTopCore(t *testing.T) {
t.Parallel()
all := []api.ContainerProcSummary{}
for _, tc := range topTestCases {
summary := api.ContainerProcSummary{
Name: "not used",
Titles: tc.titles,
Processes: tc.procs,
Service: tc.name,
Replica: "1",
}
all = append(all, summary)
t.Run(tc.name, func(t *testing.T) {
header, entries := collectTop([]api.ContainerProcSummary{summary})
assert.Equal(t, tc.header, header)
assert.Equal(t, tc.entries, entries)
var buf bytes.Buffer
err := topPrint(&buf, header, entries)
require.NoError(t, err)
assert.Equal(t, tc.output, buf.String())
})
}
t.Run("all", func(t *testing.T) {
header, entries := collectTop(all)
assert.Equal(t, topHeader{
"SERVICE": 0,
"#": 1,
"UID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"GID": 9,
"CMD": 10,
}, header)
assert.Equal(t, []topEntries{
{
"SERVICE": "simple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:01",
"CMD": "/entrypoint",
}, {
"SERVICE": "noppid",
"#": "1",
"UID": "root",
"PID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:02",
"CMD": "/entrypoint",
}, {
"SERVICE": "extra-hdr",
"#": "1",
"UID": "root",
"GID": "1",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:03",
"CMD": "/entrypoint",
}, {
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:04",
"CMD": "/entrypoint",
}, {
"SERVICE": "multiple",
"#": "1",
"UID": "root",
"PID": "123",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:42",
"CMD": "sleep infinity",
},
}, entries)
var buf bytes.Buffer
err := topPrint(&buf, header, entries)
require.NoError(t, err)
assert.Equal(t, trim(`
SERVICE # UID PID PPID C STIME TTY TIME GID CMD
simple 1 root 1 1 0 12:00 ? 00:00:01 - /entrypoint
noppid 1 root 1 - 0 12:00 ? 00:00:02 - /entrypoint
extra-hdr 1 root 1 1 0 12:00 ? 00:00:03 1 /entrypoint
multiple 1 root 1 1 0 12:00 ? 00:00:04 - /entrypoint
multiple 1 root 123 1 0 12:00 ? 00:00:42 - sleep infinity
`), buf.String())
})
}
func trim(s string) string {
var out bytes.Buffer
for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
out.WriteString(strings.TrimSpace(line))
out.WriteRune('\n')
}
return out.String()
}

View File

@ -27,7 +27,9 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
@ -145,7 +147,6 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
flags := upCmd.Flags()
flags.BoolVarP(&up.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
flags.BoolVar(&create.Build, "build", false, "Build images before starting containers")
flags.BoolVarP(&create.AssumeYes, "y", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
flags.BoolVar(&create.noBuild, "no-build", false, "Don't build an image, even if it's policy")
flags.StringVar(&create.Pull, "pull", "policy", `Pull image before running ("always"|"missing"|"never")`)
flags.BoolVar(&create.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")
@ -171,7 +172,15 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy")
flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.")
flags.BoolVar(&up.navigationMenu, "menu", false, "Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var.")
flags.BoolVarP(&create.AssumeYes, "yes", "y", false, `Assume "yes" as answer to all prompts and run non-interactively`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// assumeYes was introduced by mistake as `--y`
if name == "y" {
logrus.Warn("--y is deprecated, please use --yes instead")
name = "yes"
}
return pflag.NormalizedName(name)
})
return upCmd
}
@ -224,6 +233,10 @@ func runUp(
project *types.Project,
services []string,
) error {
if err := checksForRemoteStack(ctx, dockerCli, project, buildOptions, createOptions.AssumeYes, []string{}); err != nil {
return err
}
err := createOptions.Apply(project)
if err != nil {
return err

View File

@ -0,0 +1,76 @@
/*
Copyright 2025 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 (
"bytes"
"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"
)
func TestVersionCommand(t *testing.T) {
originalVersion := internal.Version
defer func() {
internal.Version = originalVersion
}()
internal.Version = "v9.9.9-test"
tests := []struct {
name string
args []string
want string
}{
{
name: "default",
args: []string{},
want: "Docker Compose version v9.9.9-test\n",
},
{
name: "short flag",
args: []string{"--short"},
want: "9.9.9-test\n",
},
{
name: "json flag",
args: []string{"--format", "json"},
want: `{"version":"v9.9.9-test"}` + "\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
buf := new(bytes.Buffer)
cli := mocks.NewMockCli(ctrl)
cli.EXPECT().Out().Return(streams.NewOut(buf)).AnyTimes()
cmd := versionCommand(cli)
cmd.SetArgs(test.args)
err := cmd.Execute()
assert.NilError(t, err)
assert.Equal(t, test.want, buf.String())
})
}
}

View File

@ -59,7 +59,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
}
cmd.Flags().BoolVar(&buildOpts.quiet, "quiet", false, "hide build output")
cmd.Flags().BoolVar(&watchOpts.prune, "prune", false, "Prune dangling images on rebuild")
cmd.Flags().BoolVar(&watchOpts.prune, "prune", true, "Prune dangling images on rebuild")
cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")
return cmd
}

View File

@ -238,7 +238,7 @@ func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Pro
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("Could not open Docker Desktop")
err = fmt.Errorf("could not open Docker Desktop")
lk.keyboardError("View", err)
}
return err
@ -255,7 +255,7 @@ func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Proje
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("Could not open Docker Desktop Compose UI")
err = fmt.Errorf("could not open Docker Desktop Compose UI")
lk.keyboardError("View Config", err)
}
return err
@ -269,7 +269,7 @@ func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Proje
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
err := open.Run(link)
if err != nil {
err = fmt.Errorf("Could not open Docker Desktop Compose UI")
err = fmt.Errorf("could not open Docker Desktop Compose UI")
lk.keyboardError("Watch Docs", err)
}
return err
@ -299,7 +299,7 @@ func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
if options.Create.Build == nil {
err := fmt.Errorf("Cannot run watch mode with flag --no-build")
err := fmt.Errorf("cannot run watch mode with flag --no-build")
lk.keyboardError("Watch", err)
return err
}

View File

@ -28,6 +28,7 @@ Define and run multi-container applications with Docker
| [`pause`](compose_pause.md) | Pause services |
| [`port`](compose_port.md) | Print the public port for a port binding |
| [`ps`](compose_ps.md) | List containers |
| [`publish`](compose_publish.md) | Publish compose application |
| [`pull`](compose_pull.md) | Pull service images |
| [`push`](compose_push.md) | Push service images |
| [`restart`](compose_restart.md) | Restart service containers |
@ -67,7 +68,7 @@ Define and run multi-container applications with Docker
## Examples
### Use `-f` to specify the name and path of one or more Compose files
Use the `-f` flag to specify the location of a Compose configuration file.
Use the `-f` flag to specify the location of a Compose [configuration file](/reference/compose-file/).
#### Specifying multiple Compose files
You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single
@ -77,10 +78,10 @@ to their predecessors.
For example, consider this command line:
```console
$ docker compose -f docker-compose.yml -f docker-compose.admin.yml run backup_db
$ docker compose -f compose.yaml -f compose.admin.yaml run backup_db
```
The `docker-compose.yml` file might specify a `webapp` service.
The `compose.yaml` file might specify a `webapp` service.
```yaml
services:
@ -91,7 +92,7 @@ services:
volumes:
- "/data"
```
If the `docker-compose.admin.yml` also specifies this same service, any matching fields override the previous file.
If the `compose.admin.yaml` also specifies this same service, any matching fields override the previous file.
New values, add to the `webapp` service configuration.
```yaml
@ -206,4 +207,4 @@ $ docker compose --dry-run up --build -d
From the example above, you can see that the first step is to pull the image defined by `db` service, then build the `backend` service.
Next, the containers are created. The `db` service is started, and the `backend` and `proxy` wait until the `db` service is healthy before starting.
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.
Dry Run mode works with almost all commands. You cannot use Dry Run mode with a command that doesn't change the state of a Compose stack such as `ps`, `ls`, `logs` for example.

View File

@ -11,7 +11,7 @@ Publish compose application
| `--oci-version` | `string` | | OCI image/artifact specification version (automatically determined by default) |
| `--resolve-image-digests` | `bool` | | Pin image tags to digests |
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |
| `-y`, `--y` | `bool` | | Assume "yes" as answer to all prompts |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts |
<!---MARKER_GEN_END-->

View File

@ -20,6 +20,7 @@ run `docker compose build` to rebuild it.
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-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 |
| `--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 |

View File

@ -19,6 +19,7 @@ the canonical format.
| `--hash` | `string` | | Print the service config hash, one per line. |
| `--images` | `bool` | | Print the image names, one per line. |
| `--no-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output |
| `--no-env-resolution` | `bool` | | Don't resolve service env files |
| `--no-interpolate` | `bool` | | Don't interpolate environment variables |
| `--no-normalize` | `bool` | | Don't normalize compose model |
| `--no-path-resolution` | `bool` | | Don't resolve file paths |

View File

@ -16,7 +16,7 @@ Creates containers for a service
| `--quiet-pull` | `bool` | | Pull without printing progress information |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `--scale` | `stringArray` | | Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. |
| `-y`, `--y` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively |
<!---MARKER_GEN_END-->

View File

@ -0,0 +1,18 @@
# docker compose publish
<!---MARKER_GEN_START-->
Publish compose application
### Options
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:-------------------------------------------------------------------------------|
| `--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 |
| `--with-env` | `bool` | | Include environment variables in the published OCI artifact |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts |
<!---MARKER_GEN_END-->

View File

@ -66,6 +66,7 @@ specified in the service configuration.
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--entrypoint` | `string` | | Override the entrypoint of the image |
| `-e`, `--env` | `stringArray` | | Set environment variables |
| `--env-from-file` | `stringArray` | | Set environment variables from file |
| `-i`, `--interactive` | `bool` | `true` | Keep STDIN open even if not attached |
| `-l`, `--label` | `stringArray` | | Add or override a label |
| `--name` | `string` | | Assign a name to the container |
@ -73,6 +74,8 @@ specified in the service configuration.
| `--no-deps` | `bool` | | Don't start linked services |
| `-p`, `--publish` | `stringArray` | | Publish a container's port(s) to the host |
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
| `-q`, `--quiet` | `bool` | | Don't print anything to STDOUT |
| `--quiet-build` | `bool` | | Suppress progress output from the build process |
| `--quiet-pull` | `bool` | | Pull without printing progress information |
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
| `--rm` | `bool` | | Automatically remove the container when it exits |

View File

@ -53,7 +53,7 @@ If the process is interrupted using `SIGINT` (ctrl + C) or `SIGTERM`, the contai
| `--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 |
| `-w`, `--watch` | `bool` | | Watch source code and rebuild/refresh containers when files are updated. |
| `-y`, `--y` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively |
| `-y`, `--yes` | `bool` | | Assume "yes" as answer to all prompts and run non-interactively |
<!---MARKER_GEN_END-->

View File

@ -9,7 +9,7 @@ Watch build context for service and rebuild/refresh containers when files are up
|:------------|:-------|:--------|:----------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--no-up` | `bool` | | Do not build & start services before watching |
| `--prune` | `bool` | | Prune dangling images on rebuild |
| `--prune` | `bool` | `true` | Prune dangling images on rebuild |
| `--quiet` | `bool` | | hide build output |

View File

@ -22,6 +22,7 @@ cname:
- docker compose pause
- docker compose port
- docker compose ps
- docker compose publish
- docker compose pull
- docker compose push
- docker compose restart
@ -55,6 +56,7 @@ clink:
- docker_compose_pause.yaml
- docker_compose_port.yaml
- docker_compose_ps.yaml
- docker_compose_publish.yaml
- docker_compose_pull.yaml
- docker_compose_push.yaml
- docker_compose_restart.yaml
@ -229,7 +231,7 @@ options:
swarm: false
examples: |-
### Use `-f` to specify the name and path of one or more Compose files
Use the `-f` flag to specify the location of a Compose configuration file.
Use the `-f` flag to specify the location of a Compose [configuration file](/reference/compose-file/).
#### Specifying multiple Compose files
You can supply multiple `-f` configuration files. When you supply multiple files, Compose combines them into a single
@ -239,10 +241,10 @@ examples: |-
For example, consider this command line:
```console
$ docker compose -f docker-compose.yml -f docker-compose.admin.yml run backup_db
$ docker compose -f compose.yaml -f compose.admin.yaml run backup_db
```
The `docker-compose.yml` file might specify a `webapp` service.
The `compose.yaml` file might specify a `webapp` service.
```yaml
services:
@ -253,7 +255,7 @@ examples: |-
volumes:
- "/data"
```
If the `docker-compose.admin.yml` also specifies this same service, any matching fields override the previous file.
If the `compose.admin.yaml` also specifies this same service, any matching fields override the previous file.
New values, add to the `webapp` service configuration.
```yaml

View File

@ -35,7 +35,7 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: "y"
- option: "yes"
shorthand: "y"
value_type: bool
default_value: "false"

View File

@ -96,6 +96,16 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: print
value_type: bool
default_value: "false"
description: Print equivalent bake file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: progress
value_type: string
default_value: auto

View File

@ -59,6 +59,16 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: no-env-resolution
value_type: bool
default_value: "false"
description: Don't resolve service env files
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-interpolate
value_type: bool
default_value: "false"

View File

@ -88,7 +88,7 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: "y"
- option: "yes"
shorthand: "y"
value_type: bool
default_value: "false"

View File

@ -0,0 +1,66 @@
command: docker compose publish
short: Publish compose application
long: Publish compose application
usage: docker compose publish [OPTIONS] REPOSITORY[:TAG]
pname: docker compose
plink: docker_compose.yaml
options:
- option: oci-version
value_type: string
description: |
OCI image/artifact specification version (automatically determined by default)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: resolve-image-digests
value_type: bool
default_value: "false"
description: Pin image tags to digests
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: with-env
value_type: bool
default_value: "false"
description: Include environment variables in the published OCI artifact
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: "yes"
shorthand: "y"
value_type: bool
default_value: "false"
description: Assume "yes" as answer to all prompts
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

View File

@ -117,6 +117,16 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: env-from-file
value_type: stringArray
default_value: '[]'
description: Set environment variables from file
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: interactive
shorthand: i
value_type: bool
@ -190,6 +200,27 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Don't print anything to STDOUT
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-build
value_type: bool
default_value: "false"
description: Suppress progress output from the build process
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet-pull
value_type: bool
default_value: "false"

View File

@ -309,7 +309,7 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: "y"
- option: "yes"
shorthand: "y"
value_type: bool
default_value: "false"

View File

@ -19,7 +19,7 @@ options:
swarm: false
- option: prune
value_type: bool
default_value: "false"
default_value: "true"
description: Prune dangling images on rebuild
deprecated: false
hidden: false

61
go.mod
View File

@ -1,38 +1,39 @@
module github.com/docker/compose/v2
go 1.22.10
go 1.23.8
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e
github.com/Microsoft/go-winio v0.6.2
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/buger/goterm v1.0.4
github.com/compose-spec/compose-go/v2 v2.4.8
github.com/containerd/containerd/v2 v2.0.2
github.com/compose-spec/compose-go/v2 v2.5.0
github.com/containerd/containerd/v2 v2.0.4
github.com/containerd/platforms v1.0.0-rc.1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/distribution/reference v0.6.0
github.com/docker/buildx v0.21.1
github.com/docker/cli v28.0.0+incompatible
github.com/docker/buildx v0.22.0
github.com/docker/cli v28.0.4+incompatible
github.com/docker/cli-docs-tool v0.9.0
github.com/docker/docker v28.0.0+incompatible
github.com/docker/docker v28.0.4+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
github.com/fsnotify/fsevents v0.2.0
github.com/google/go-cmp v0.6.0
github.com/google/go-cmp v0.7.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.7.0
github.com/jonboulle/clockwork v0.5.0
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-ps v1.0.0
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/buildkit v0.20.0
github.com/moby/buildkit v0.20.1
github.com/moby/patternmatcher v0.6.0
github.com/moby/term v0.5.2
github.com/morikuni/aec v1.0.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
github.com/opencontainers/image-spec v1.1.1
github.com/otiai10/copy v1.14.1
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc
github.com/sirupsen/logrus v1.9.3
@ -43,21 +44,21 @@ require (
github.com/theupdateframework/notary v0.7.0
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0
go.opentelemetry.io/otel v1.32.0
go.opentelemetry.io/otel v1.34.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0
go.opentelemetry.io/otel/metric v1.32.0
go.opentelemetry.io/otel/sdk v1.32.0
go.opentelemetry.io/otel/trace v1.32.0
go.opentelemetry.io/otel/metric v1.34.0
go.opentelemetry.io/otel/sdk v1.34.0
go.opentelemetry.io/otel/trace v1.34.0
go.uber.org/goleak v1.3.0
go.uber.org/mock v0.5.0
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
golang.org/x/sync v0.11.0
golang.org/x/sys v0.30.0
google.golang.org/grpc v1.70.0
golang.org/x/sync v0.13.0
golang.org/x/sys v0.32.0
google.golang.org/grpc v1.71.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v0.8.0
tags.cncf.io/container-device-interface v1.0.1
)
require (
@ -107,6 +108,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
@ -120,13 +122,15 @@ require (
github.com/imdario/mergo v0.3.16 // indirect
github.com/in-toto/in-toto-golang v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
@ -168,25 +172,28 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/zclconf/go-cty v1.16.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
google.golang.org/protobuf v1.35.2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.31.2 // indirect
k8s.io/apimachinery v0.31.2 // indirect
@ -198,3 +205,5 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
replace github.com/compose-spec/compose-go/v2 => github.com/glours/compose-go/v2 v2.0.0-20250403082600-80aa75f06535

124
go.sum
View File

@ -10,6 +10,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@ -81,16 +83,14 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.4.8 h1:7Myl8wDRl/4mRz77S+eyDJymGGEHu0diQdGSSeyq90A=
github.com/compose-spec/compose-go/v2 v2.4.8/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0=
github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc=
github.com/containerd/containerd/v2 v2.0.2 h1:GmH/tRBlTvrXOLwSpWE2vNAm8+MqI6nmxKpKBNKY8Wc=
github.com/containerd/containerd/v2 v2.0.2/go.mod h1:wIqEvQ/6cyPFUGJ5yMFanspPabMLor+bF865OHvNTTI=
github.com/containerd/containerd/v2 v2.0.4 h1:+r7yJMwhTfMm3CDyiBjMBQO8a9CTBxL2Bg/JtqtIwB8=
github.com/containerd/containerd/v2 v2.0.4/go.mod h1:5j9QUUaV/cy9ZeAx4S+8n9ffpf+iYnEj4jiExgcbuLY=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@ -127,17 +127,17 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/buildx v0.21.1 h1:YjV2k6CsSDbkDTOMsjARUIrj2xv+zZR+M2dtrRyzXhg=
github.com/docker/buildx v0.21.1/go.mod h1:8V4UMnlKsaGYwz83BygmIbJIFEAYGHT6KAv8akDZmqo=
github.com/docker/cli v28.0.0+incompatible h1:ido37VmLUqEp+5NFb9icd6BuBB+SNDgCn+5kPCr2buA=
github.com/docker/cli v28.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/buildx v0.22.0 h1:pGTcGZa+kxpYUlM/6ACsp1hXhkEDulz++RNXPdE8Afk=
github.com/docker/buildx v0.22.0/go.mod h1:ThbnUe4kNiStlq6cLXruElyEdSTdPL3k/QerNUmPvHE=
github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A=
github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.9.0 h1:CVwQbE+ZziwlPqrJ7LRyUF6GvCA+6gj7MTCsayaK9t0=
github.com/docker/cli-docs-tool v0.9.0/go.mod h1:ClrwlNW+UioiRyH9GiAOe1o3J/TsY3Tr1ipoypjAUtc=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM=
github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@ -163,12 +163,12 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/glours/compose-go/v2 v2.0.0-20250403082600-80aa75f06535 h1:S/P6v3QxsMpkKn+2OSMPNkfSkadSjSHoMGAc/eBZgMU=
github.com/glours/compose-go/v2 v2.0.0-20250403082600-80aa75f06535/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@ -197,6 +197,8 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -212,8 +214,8 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -251,6 +253,8 @@ github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1Gd
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
@ -286,8 +290,9 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -295,8 +300,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
@ -315,8 +320,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/buildkit v0.20.0 h1:aF5RujjQ310Pn6SLL/wQYIrSsPXy0sQ5KvWifwq1h8Y=
github.com/moby/buildkit v0.20.0/go.mod h1:HYFUIK+iGDRxRgdphZ9Nv0y1Fz7mv0HrU7xZoXx217E=
github.com/moby/buildkit v0.20.1 h1:sT0ZXhhNo5rVbMcYfgttma3TdUHfO5JjFA0UAL8p9fY=
github.com/moby/buildkit v0.20.1/go.mod h1:Rq9nB/fJImdk6QeM0niKtOHJqwKeYMrK847hTTDVuA4=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
@ -369,12 +374,10 @@ github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0=
github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI=
github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8=
github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
@ -467,8 +470,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
@ -494,6 +495,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@ -501,14 +504,16 @@ github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w
github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM=
@ -519,14 +524,14 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -541,8 +546,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@ -560,10 +565,10 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -571,8 +576,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -593,13 +598,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -619,15 +625,15 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
@ -642,6 +648,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@ -674,7 +682,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+s
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
tags.cncf.io/container-device-interface v0.8.0 h1:8bCFo/g9WODjWx3m6EYl3GfUG31eKJbaggyBDxEldRc=
tags.cncf.io/container-device-interface v0.8.0/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y=
tags.cncf.io/container-device-interface/specs-go v0.8.0 h1:QYGFzGxvYK/ZLMrjhvY0RjpUavIn4KcmRmVP/JjdBTA=
tags.cncf.io/container-device-interface/specs-go v0.8.0/go.mod h1:BhJIkjjPh4qpys+qm4DAYtUyryaTDg9zris+AczXyws=
tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc=
tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0=

View File

@ -155,6 +155,8 @@ type BuildOptions struct {
Memory int64
// Builder name passed in the command line
Builder string
// Print don't actually run builder but print equivalent build config
Print bool
}
// Apply mutates project according to build options
@ -523,6 +525,8 @@ type ContainerProcSummary struct {
Name string
Processes [][]string
Titles []string
Service string
Replica string
}
// ImageSummary holds container image description
@ -532,6 +536,7 @@ type ImageSummary struct {
Repository string
Tag string
Size int64
LastTagTime time.Time
}
// ServiceStatus hold status about a service

View File

@ -23,6 +23,7 @@ import (
"os"
"strings"
"sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
@ -49,6 +50,8 @@ import (
"github.com/moby/buildkit/util/progress/progressui"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
// required to get default driver registered
_ "github.com/docker/buildx/driver/docker"
@ -60,17 +63,20 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
return err
}
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
_, err := s.build(ctx, project, options, nil)
return err
return tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project),
func(ctx context.Context) error {
_, err := s.build(ctx, project, options, nil)
return err
})(ctx)
}, s.stdinfo(), "Building")
}
const bakeSuggest = "Compose now can delegate build to bake for better performances\nJust set COMPOSE_BAKE=true"
const bakeSuggest = "Compose can now delegate builds to bake for better performance.\n To do so, set COMPOSE_BAKE=true."
var suggest sync.Once
//nolint:gocyclo
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]string) (map[string]string, error) {
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]api.ImageSummary) (map[string]string, error) {
imageIDs := map[string]string{}
serviceToBuild := types.Services{}
@ -79,32 +85,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
policy = types.IncludeDependencies
}
serviceDeps := false
project, err := project.WithServicesTransform(func(serviceName string, service types.ServiceConfig) (types.ServiceConfig, error) {
if service.Build != nil {
for _, c := range service.Build.AdditionalContexts {
if t, found := strings.CutPrefix(c, types.ServicePrefix); found {
serviceDeps = true
if service.DependsOn == nil {
service.DependsOn = map[string]types.ServiceDependency{}
}
service.DependsOn[t] = types.ServiceDependency{
Condition: "build", // non-canonical, but will force dependency graph ordering
}
}
}
}
return service, nil
})
if err != nil {
return imageIDs, err
}
if serviceDeps {
logrus.Infof(`additional_context with "service:"" is better supported when delegating build go bake. Set COMPOSE_BAKE=true`)
}
err = project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
err := project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
if service.Build == nil {
return nil
}
@ -124,10 +105,31 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
if err != nil {
return nil, err
}
if bake {
if bake || options.Print {
trace.SpanFromContext(ctx).SetAttributes(attribute.String("builder", "bake"))
return s.doBuildBake(ctx, project, serviceToBuild, options)
}
// Not using bake, additional_context: service:xx is implemented by building images in dependency order
project, err = project.WithServicesTransform(func(serviceName string, service types.ServiceConfig) (types.ServiceConfig, error) {
if service.Build != nil {
for _, c := range service.Build.AdditionalContexts {
if t, found := strings.CutPrefix(c, types.ServicePrefix); found {
if service.DependsOn == nil {
service.DependsOn = map[string]types.ServiceDependency{}
}
service.DependsOn[t] = types.ServiceDependency{
Condition: "build", // non-canonical, but will force dependency graph ordering
}
}
}
}
return service, nil
})
if err != nil {
return imageIDs, err
}
// Initialize buildkit nodes
buildkitEnabled, err := s.dockerCli.BuildKitEnabled()
if err != nil {
@ -201,6 +203,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
serviceName := fmt.Sprintf("Service %s", name)
if !buildkitEnabled {
trace.SpanFromContext(ctx).SetAttributes(attribute.String("builder", "classic"))
cw.Event(progress.BuildingEvent(serviceName))
id, err := s.doBuildClassic(ctx, project, service, options)
if err != nil {
@ -224,6 +227,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
return err
}
trace.SpanFromContext(ctx).SetAttributes(attribute.String("builder", "buildkit"))
digest, err := s.doBuildBuildkit(ctx, name, buildOptions, w, nodes)
if err != nil {
return err
@ -259,7 +263,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, buildOpts *api.BuildOptions, quietPull bool) error {
for name, service := range project.Services {
if service.Image == "" && service.Build == nil {
if service.Provider == nil && service.Image == "" && service.Build == nil {
return fmt.Errorf("invalid service %q. Must specify either image or build", name)
}
}
@ -287,7 +291,11 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
}
for name, digest := range builtImages {
images[name] = digest
images[name] = api.ImageSummary{
Repository: name,
ID: digest,
LastTagTime: time.Now(),
}
}
return nil
},
@ -300,19 +308,16 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
// set digest as com.docker.compose.image label so we can detect outdated containers
for name, service := range project.Services {
image := api.GetImageNameOrDefault(service, project.Name)
digest, ok := images[image]
img, ok := images[image]
if ok {
if service.Labels == nil {
service.Labels = types.Labels{}
}
service.CustomLabels.Add(api.ImageDigestLabel, digest)
service.CustomLabels.Add(api.ImageDigestLabel, img.ID)
}
project.Services[name] = service
}
return nil
}
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]api.ImageSummary, error) {
var imageNames []string
for _, s := range project.Services {
imgName := api.GetImageNameOrDefault(s, project.Name)
@ -324,14 +329,10 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
if err != nil {
return nil, err
}
images := map[string]string{}
for name, info := range imgs {
images[name] = info.ID
}
for i, service := range project.Services {
imgName := api.GetImageNameOrDefault(service, project.Name)
digest, ok := images[imgName]
img, ok := imgs[imgName]
if !ok {
continue
}
@ -340,7 +341,7 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
if err != nil {
return nil, err
}
inspect, err := s.apiClient().ImageInspect(ctx, digest)
inspect, err := s.apiClient().ImageInspect(ctx, img.ID)
if err != nil {
return nil, err
}
@ -353,15 +354,15 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
// there is a local image, but it's for the wrong platform, so
// pretend it doesn't exist so that we can pull/build an image
// for the correct platform instead
delete(images, imgName)
delete(imgs, imgName)
}
}
project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest)
project.Services[i].CustomLabels.Add(api.ImageDigestLabel, img.ID)
}
return images, nil
return imgs, nil
}
// resolveAndMergeBuildArgs returns the final set of build arguments to use for the service image build.
@ -468,7 +469,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
ContextPath: service.Build.Context,
DockerfileInline: service.Build.DockerfileInline,
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
NamedContexts: toBuildContexts(service.Build.AdditionalContexts),
NamedContexts: toBuildContexts(service, project),
},
CacheFrom: pb.CreateCaches(cacheFrom.ToPB()),
CacheTo: pb.CreateCaches(cacheTo.ToPB()),
@ -573,13 +574,15 @@ func getImageBuildLabels(project *types.Project, service types.ServiceConfig) ty
return ret
}
func toBuildContexts(additionalContexts types.Mapping) map[string]build.NamedContext {
func toBuildContexts(service types.ServiceConfig, project *types.Project) map[string]build.NamedContext {
namedContexts := map[string]build.NamedContext{}
for name, contextPath := range additionalContexts {
if _, found := strings.CutPrefix(contextPath, types.ServicePrefix); found {
// image we depend on has been build previously, as we run in dependency order.
// this assumes use of docker engine builder, so that build can access local images
continue
for name, contextPath := range service.Build.AdditionalContexts {
if strings.HasPrefix(contextPath, types.ServicePrefix) {
// image we depend on has been built previously, as we run in dependency order.
// so we convert the service reference into an image reference
target := contextPath[len(types.ServicePrefix):]
image := api.GetImageNameOrDefault(project.Services[target], project.Name)
contextPath = "docker-image://" + image
}
namedContexts[name] = build.NamedContext{Path: contextPath}
}

View File

@ -191,7 +191,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
Context: build.Context,
Contexts: additionalContexts(build.AdditionalContexts),
Dockerfile: dockerFilePath(build.Context, build.Dockerfile),
DockerfileInline: build.DockerfileInline,
DockerfileInline: strings.ReplaceAll(build.DockerfileInline, "${", "$${"),
Args: args,
Labels: build.Labels,
Tags: append(build.Tags, image),
@ -219,6 +219,10 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
return nil, err
}
if options.Print {
_, err = fmt.Fprintln(s.stdinfo(), string(b))
return nil, err
}
logrus.Debugf("bake build config:\n%s", string(b))
metadata, err := os.CreateTemp(os.TempDir(), "compose")

View File

@ -47,7 +47,7 @@ import (
const (
doubledContainerNameWarning = "WARNING: The %q service is using the custom container name %q. " +
"Docker requires each container to have a unique name. " +
"Remove the custom name to scale the service.\n"
"Remove the custom name to scale the service"
)
// convergence manages service's container lifecycle.
@ -110,6 +110,9 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options
}
func (c *convergence) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string, inherit bool, timeout *time.Duration) error { //nolint:gocyclo
if service.Provider != nil {
return c.service.runPlugin(ctx, project, service, "up")
}
expected, err := getScale(service)
if err != nil {
return err
@ -225,7 +228,9 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
func (c *convergence) stopDependentContainers(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
// Stop dependent containers, so they will be restarted after service is re-created
dependents := project.GetDependentsForService(service)
dependents := project.GetDependentsForService(service, func(dependency types.ServiceDependency) bool {
return dependency.Restart
})
if len(dependents) == 0 {
return nil
}

View File

@ -486,7 +486,7 @@ func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, bool,
if strings.Contains(opt, ":") {
con = strings.SplitN(opt, ":", 2)
} else {
return securityOpts, false, fmt.Errorf("Invalid security-opt: %q", opt)
return securityOpts, false, fmt.Errorf("invalid security-opt: %q", opt)
}
}
if con[0] == "seccomp" && con[1] != "unconfined" && con[1] != "builtin" {
@ -997,10 +997,10 @@ func buildContainerConfigMounts(p types.Project, s types.ServiceConfig) ([]mount
}
if definedConfig.Driver != "" {
return nil, errors.New("Docker Compose does not support configs.*.driver")
return nil, errors.New("Docker Compose does not support configs.*.driver") //nolint:staticcheck
}
if definedConfig.TemplateDriver != "" {
return nil, errors.New("Docker Compose does not support configs.*.template_driver")
return nil, errors.New("Docker Compose does not support configs.*.template_driver") //nolint:staticcheck
}
if definedConfig.Environment != "" || definedConfig.Content != "" {
@ -1047,10 +1047,10 @@ func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount
}
if definedSecret.Driver != "" {
return nil, errors.New("Docker Compose does not support secrets.*.driver")
return nil, errors.New("Docker Compose does not support secrets.*.driver") //nolint:staticcheck
}
if definedSecret.TemplateDriver != "" {
return nil, errors.New("Docker Compose does not support secrets.*.template_driver")
return nil, errors.New("Docker Compose does not support secrets.*.template_driver") //nolint:staticcheck
}
if definedSecret.Environment != "" {

View File

@ -83,8 +83,11 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
}
err = InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
serviceContainers := containers.filter(isService(service))
serv := project.Services[service]
if serv.Provider != nil {
return s.runPlugin(ctx, project, serv, "down")
}
serviceContainers := containers.filter(isService(service))
err := s.removeContainers(ctx, serviceContainers, &serv, options.Timeout, options.Volumes)
return err
}, WithRootNodesAndDown(options.Services))

View File

@ -101,10 +101,11 @@ func (s *composeService) getImageSummaries(ctx context.Context, repoTags []strin
}
l.Lock()
summary[repoTag] = api.ImageSummary{
ID: inspect.ID,
Repository: repository,
Tag: tag,
Size: inspect.Size,
ID: inspect.ID,
Repository: repository,
Tag: tag,
Size: inspect.Size,
LastTagTime: inspect.Metadata.LastTagTime,
}
l.Unlock()
return nil

View File

@ -70,7 +70,7 @@ func combinedConfigFiles(containers []container.Summary) (string, error) {
for _, c := range containers {
files, ok := c.Labels[api.ConfigFilesLabel]
if !ok {
return "", fmt.Errorf("No label %q set on container %q of compose project", api.ConfigFilesLabel, c.ID)
return "", fmt.Errorf("no label %q set on container %q of compose project", api.ConfigFilesLabel, c.ID)
}
for _, f := range strings.Split(files, ",") {
@ -120,7 +120,7 @@ func groupContainerByLabel(containers []container.Summary, labelName string) (ma
for _, c := range containers {
label, ok := c.Labels[labelName]
if !ok {
return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID)
return nil, nil, fmt.Errorf("no label %q set on container %q of compose project", labelName, c.ID)
}
labelContainers, ok := containersByLabel[label]
if !ok {

View File

@ -104,7 +104,7 @@ func TestCombinedConfigFiles(t *testing.T) {
}{
"project1": {ConfigFiles: "/home/docker-compose.yaml", Error: nil},
"project2": {ConfigFiles: "/home/project2-docker-compose.yaml", Error: nil},
"project3": {ConfigFiles: "", Error: fmt.Errorf("No label %q set on container %q of compose project", api.ConfigFilesLabel, "service4")},
"project3": {ConfigFiles: "", Error: fmt.Errorf("no label %q set on container %q of compose project", api.ConfigFilesLabel, "service4")},
}
for project, containers := range containersByLabel {

179
pkg/compose/plugins.go Normal file
View File

@ -0,0 +1,179 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"golang.org/x/sync/errgroup"
)
type JsonMessage struct {
Type string `json:"type"`
Message string `json:"message"`
}
const (
ErrorType = "error"
InfoType = "info"
SetEnvType = "setenv"
)
func (s *composeService) runPlugin(ctx context.Context, project *types.Project, service types.ServiceConfig, command string) error {
provider := *service.Provider
plugin, err := s.getPluginBinaryPath(provider.Type)
if err != nil {
return err
}
if err := s.checkPluginEnabledInDD(ctx, plugin); err != nil {
return err
}
cmd := s.setupPluginCommand(ctx, project, provider, plugin.Path, command)
eg := errgroup.Group{}
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
eg.Go(cmd.Wait)
decoder := json.NewDecoder(stdout)
defer func() { _ = stdout.Close() }()
variables := types.Mapping{}
pw := progress.ContextWriter(ctx)
pw.Event(progress.CreatingEvent(service.Name))
for {
var msg JsonMessage
err = decoder.Decode(&msg)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
switch msg.Type {
case ErrorType:
pw.Event(progress.ErrorMessageEvent(service.Name, "error"))
return errors.New(msg.Message)
case InfoType:
pw.Event(progress.ErrorMessageEvent(service.Name, msg.Message))
case SetEnvType:
key, val, found := strings.Cut(msg.Message, "=")
if !found {
return fmt.Errorf("invalid response from plugin: %s", msg.Message)
}
variables[key] = val
default:
return fmt.Errorf("invalid response from plugin: %s", msg.Type)
}
}
err = eg.Wait()
if err != nil {
pw.Event(progress.ErrorMessageEvent(service.Name, err.Error()))
return fmt.Errorf("failed to create external service: %s", err.Error())
}
pw.Event(progress.CreatedEvent(service.Name))
prefix := strings.ToUpper(service.Name) + "_"
for name, s := range project.Services {
if _, ok := s.DependsOn[service.Name]; ok {
for key, val := range variables {
s.Environment[prefix+key] = &val
}
project.Services[name] = s
}
}
return nil
}
func (s *composeService) getPluginBinaryPath(providerType string) (*manager.Plugin, error) {
// Only support Docker CLI plugins for first iteration. Could support any binary from PATH
return manager.GetPlugin(providerType, s.dockerCli, &cobra.Command{})
}
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, provider types.ServiceProviderConfig, path, command string) *exec.Cmd {
args := []string{"compose", "--project-name", project.Name, command}
for k, v := range provider.Options {
args = append(args, fmt.Sprintf("--%s=%s", k, v))
}
cmd := exec.CommandContext(ctx, path, args...)
// Remove DOCKER_CLI_PLUGIN... variable so plugin can detect it run standalone
cmd.Env = filter(os.Environ(), manager.ReexecEnvvar)
// Use docker/cli mechanism to propagate termination signal to child process
server, err := socket.NewPluginServer(nil)
if err == nil {
defer server.Close() //nolint:errcheck
cmd.Cancel = server.Close
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
}
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, &carrier)
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
return cmd
}
func (s *composeService) checkPluginEnabledInDD(ctx context.Context, plugin *manager.Plugin) error {
if integrationEnabled := s.isDesktopIntegrationActive(); !integrationEnabled {
return fmt.Errorf("you should enable Docker Desktop integration to use %q provider services", plugin.Name)
}
// Until we support more use cases, check explicitly status of model runner
if plugin.Name == "model" {
cmd := exec.CommandContext(ctx, "docker", "model", "status")
_, err := cmd.CombinedOutput()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
return fmt.Errorf("you should enable model runner to use %q provider services: %s", plugin.Name, err.Error())
}
}
} else {
return fmt.Errorf("unsupported provider %q", plugin.Name)
}
return nil
}

View File

@ -17,16 +17,25 @@
package compose
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
"github.com/DefangLabs/secret-detector/pkg/scanner"
"github.com/DefangLabs/secret-detector/pkg/secrets"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/distribution/reference"
"github.com/docker/buildx/util/imagetools"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/internal/ocipush"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose/transform"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
)
@ -60,19 +69,26 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
})
var layers []ocipush.Pushable
extFiles := map[string]string{}
for _, file := range project.ComposeFiles {
f, err := os.ReadFile(file)
data, err := processFile(ctx, file, project, extFiles)
if err != nil {
return err
}
layerDescriptor := ocipush.DescriptorForComposeFile(file, f)
layerDescriptor := ocipush.DescriptorForComposeFile(file, data)
layers = append(layers, ocipush.Pushable{
Descriptor: layerDescriptor,
Data: f,
Data: data,
})
}
extLayers, err := processExtends(ctx, project, extFiles)
if err != nil {
return err
}
layers = append(layers, extLayers...)
if options.WithEnvironment {
layers = append(layers, envFileLayers(project)...)
}
@ -115,6 +131,86 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
return nil
}
func processExtends(ctx context.Context, project *types.Project, extFiles map[string]string) ([]ocipush.Pushable, error) {
var layers []ocipush.Pushable
moreExtFiles := map[string]string{}
for xf, hash := range extFiles {
data, err := processFile(ctx, xf, project, moreExtFiles)
if err != nil {
return nil, err
}
layerDescriptor := ocipush.DescriptorForComposeFile(hash, data)
layerDescriptor.Annotations["com.docker.compose.extends"] = "true"
layers = append(layers, ocipush.Pushable{
Descriptor: layerDescriptor,
Data: data,
})
}
for f, hash := range moreExtFiles {
if _, ok := extFiles[f]; ok {
delete(moreExtFiles, f)
}
extFiles[f] = hash
}
if len(moreExtFiles) > 0 {
extLayers, err := processExtends(ctx, project, moreExtFiles)
if err != nil {
return nil, err
}
layers = append(layers, extLayers...)
}
return layers, nil
}
func processFile(ctx context.Context, file string, project *types.Project, extFiles map[string]string) ([]byte, error) {
f, err := os.ReadFile(file)
if err != nil {
return nil, err
}
base, err := loader.LoadWithContext(ctx, types.ConfigDetails{
WorkingDir: project.WorkingDir,
Environment: project.Environment,
ConfigFiles: []types.ConfigFile{
{
Filename: file,
Content: f,
},
},
}, func(options *loader.Options) {
options.SkipValidation = true
options.SkipExtends = true
options.SkipConsistencyCheck = true
options.ResolvePaths = true
})
if err != nil {
return nil, err
}
for name, service := range base.Services {
if service.Extends == nil {
continue
}
xf := service.Extends.File
if xf == "" {
continue
}
if _, err = os.Stat(service.Extends.File); os.IsNotExist(err) {
// No local file, while we loaded the project successfully: This is actually a remote resource
continue
}
hash := fmt.Sprintf("%x.yaml", sha256.Sum256([]byte(xf)))
extFiles[xf] = hash
f, err = transform.ReplaceExtendsFile(f, name, hash)
if err != nil {
return nil, err
}
}
return f, nil
}
func (s *composeService) generateImageDigestsOverride(ctx context.Context, project *types.Project) ([]byte, error) {
project, err := project.WithProfiles([]string{"*"})
if err != nil {
@ -135,12 +231,37 @@ func (s *composeService) generateImageDigestsOverride(ctx context.Context, proje
return override.MarshalYAML()
}
//nolint:gocyclo
func (s *composeService) preChecks(project *types.Project, options api.PublishOptions) (bool, error) {
if ok, err := s.checkOnlyBuildSection(project); !ok || err != nil {
return false, err
}
if ok, err := s.checkForBindMount(project); !ok || err != nil {
return false, err
}
if options.AssumeYes {
return true, nil
}
detectedSecrets, err := s.checkForSensitiveData(project)
if err != nil {
return false, err
}
if len(detectedSecrets) > 0 {
fmt.Println("you are about to publish sensitive data within your OCI artifact.\n" +
"please double check that you are not leaking sensitive data")
for _, val := range detectedSecrets {
_, _ = fmt.Fprintln(s.dockerCli.Out(), val.Type)
_, _ = fmt.Fprintf(s.dockerCli.Out(), "%q: %s\n", val.Key, val.Value)
}
if ok, err := acceptPublishSensitiveData(s.dockerCli); err != nil || !ok {
return false, err
}
}
envVariables, err := s.checkEnvironmentVariables(project, options)
if err != nil {
return false, err
}
if !options.AssumeYes && len(envVariables) > 0 {
if len(envVariables) > 0 {
fmt.Println("you are about to publish environment variables within your OCI artifact.\n" +
"please double check that you are not leaking sensitive data")
for key, val := range envVariables {
@ -149,7 +270,9 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp
_, _ = fmt.Fprintf(s.dockerCli.Out(), "%s=%v\n", k, *v)
}
}
return acceptPublishEnvVariables(s.dockerCli)
if ok, err := acceptPublishEnvVariables(s.dockerCli); err != nil || !ok {
return false, err
}
}
return true, nil
}
@ -196,6 +319,12 @@ func acceptPublishEnvVariables(cli command.Cli) (bool, error) {
return confirm, err
}
func acceptPublishSensitiveData(cli command.Cli) (bool, error) {
msg := "Are you ok to publish these sensitive data? [y/N]: "
confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false)
return confirm, err
}
func envFileLayers(project *types.Project) []ocipush.Pushable {
var layers []ocipush.Pushable
for _, service := range project.Services {
@ -214,3 +343,116 @@ func envFileLayers(project *types.Project) []ocipush.Pushable {
}
return layers
}
func (s *composeService) checkOnlyBuildSection(project *types.Project) (bool, error) {
errorList := []string{}
for _, service := range project.Services {
if service.Image == "" && service.Build != nil {
errorList = append(errorList, service.Name)
}
}
if len(errorList) > 0 {
errMsg := "your Compose stack cannot be published as it only contains a build section for service(s):\n"
for _, serviceInError := range errorList {
errMsg += fmt.Sprintf("- %q\n", serviceInError)
}
return false, errors.New(errMsg)
}
return true, nil
}
func (s *composeService) checkForBindMount(project *types.Project) (bool, error) {
for name, config := range project.Services {
for _, volume := range config.Volumes {
if volume.Type == types.VolumeTypeBind {
return false, fmt.Errorf("cannot publish compose file: service %q relies on bind-mount. You should use volumes", name)
}
}
}
return true, nil
}
func (s *composeService) checkForSensitiveData(project *types.Project) ([]secrets.DetectedSecret, error) {
var allFindings []secrets.DetectedSecret
scan := scanner.NewDefaultScanner()
// Check all compose files
for _, file := range project.ComposeFiles {
in, err := composeFileAsByteReader(file, project)
if err != nil {
return nil, err
}
findings, err := scan.ScanReader(in)
if err != nil {
return nil, fmt.Errorf("failed to scan compose file %s: %w", file, err)
}
allFindings = append(allFindings, findings...)
}
for _, service := range project.Services {
// Check env files
for _, envFile := range service.EnvFiles {
findings, err := scan.ScanFile(envFile.Path)
if err != nil {
return nil, fmt.Errorf("failed to scan env file %s: %w", envFile.Path, err)
}
allFindings = append(allFindings, findings...)
}
}
// Check configs defined by files
for _, config := range project.Configs {
if config.File != "" {
findings, err := scan.ScanFile(config.File)
if err != nil {
return nil, fmt.Errorf("failed to scan config file %s: %w", config.File, err)
}
allFindings = append(allFindings, findings...)
}
}
// Check secrets defined by files
for _, secret := range project.Secrets {
if secret.File != "" {
findings, err := scan.ScanFile(secret.File)
if err != nil {
return nil, fmt.Errorf("failed to scan secret file %s: %w", secret.File, err)
}
allFindings = append(allFindings, findings...)
}
}
return allFindings, nil
}
func composeFileAsByteReader(filePath string, project *types.Project) (io.Reader, error) {
composeFile, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open compose file %s: %w", filePath, err)
}
base, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{
WorkingDir: project.WorkingDir,
Environment: project.Environment,
ConfigFiles: []types.ConfigFile{
{
Filename: filePath,
Content: composeFile,
},
},
}, func(options *loader.Options) {
options.SkipValidation = true
options.SkipExtends = true
options.SkipConsistencyCheck = true
options.ResolvePaths = true
options.SkipInterpolation = true
options.SkipResolveEnvironment = true
})
if err != nil {
return nil, err
}
in, err := base.MarshalYAML()
if err != nil {
return nil, err
}
return bytes.NewBuffer(in), nil
}

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 (
"context"
"os"
"testing"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/ocipush"
"github.com/docker/compose/v2/pkg/api"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
)
func Test_processExtends(t *testing.T) {
project, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{
WorkingDir: "testdata/publish/",
Environment: types.Mapping{},
ConfigFiles: []types.ConfigFile{
{
Filename: "testdata/publish/compose.yaml",
},
},
})
assert.NilError(t, err)
extFiles := map[string]string{}
file, err := processFile(context.TODO(), "testdata/publish/compose.yaml", project, extFiles)
assert.NilError(t, err)
v := string(file)
assert.Equal(t, v, `name: test
services:
test:
extends:
file: f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml
service: foo
`)
layers, err := processExtends(context.TODO(), project, extFiles)
assert.NilError(t, err)
b, err := os.ReadFile("testdata/publish/common.yaml")
assert.NilError(t, err)
assert.DeepEqual(t, []ocipush.Pushable{
{
Descriptor: v1.Descriptor{
MediaType: "application/vnd.docker.compose.file+yaml",
Digest: "sha256:d3ba84507b56ec783f4b6d24306b99a15285f0a23a835f0b668c2dbf9c59c241",
Size: 32,
Annotations: map[string]string{
"com.docker.compose.extends": "true",
"com.docker.compose.file": "f8f9ede3d201ec37d5a5e3a77bbadab79af26035e53135e19571f50d541d390c.yaml",
"com.docker.compose.version": api.ComposeVersion,
},
},
Data: b,
},
}, layers)
}

View File

@ -24,6 +24,7 @@ import (
"fmt"
"io"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/distribution/reference"
@ -153,7 +154,7 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
return multierror.Append(nil, pullErrors...).ErrorOrNil()
}
func imageAlreadyPresent(serviceImage string, localImages map[string]string) bool {
func imageAlreadyPresent(serviceImage string, localImages map[string]api.ImageSummary) bool {
normalizedImage, err := reference.ParseDockerRef(serviceImage)
if err != nil {
return false
@ -288,23 +289,16 @@ func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) {
return base64.URLEncoding.EncodeToString(buf), nil
}
func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]string, quietPull bool) error {
func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]api.ImageSummary, quietPull bool) error {
var needPull []types.ServiceConfig
for _, service := range project.Services {
if service.Image == "" {
continue
pull, err := mustPull(service, images)
if err != nil {
return err
}
switch service.PullPolicy {
case "", types.PullPolicyMissing, types.PullPolicyIfNotPresent:
if _, ok := images[service.Image]; ok {
continue
}
case types.PullPolicyNever, types.PullPolicyBuild:
continue
case types.PullPolicyAlways:
// force pull
if pull {
needPull = append(needPull, service)
}
needPull = append(needPull, service)
}
if len(needPull) == 0 {
return nil
@ -314,11 +308,15 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types.
w := progress.ContextWriter(ctx)
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(s.maxConcurrency)
pulledImages := make([]string, len(needPull))
pulledImages := make([]api.ImageSummary, len(needPull))
for i, service := range needPull {
eg.Go(func() error {
id, err := s.pullServiceImage(ctx, service, s.configFile(), w, quietPull, project.Environment["DOCKER_DEFAULT_PLATFORM"])
pulledImages[i] = id
pulledImages[i] = api.ImageSummary{
ID: id,
Repository: service.Image,
LastTagTime: time.Now(),
}
if err != nil && isServiceImageToBuild(service, project.Services) {
// image can be built, so we can ignore pull failure
return nil
@ -328,7 +326,7 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types.
}
err := eg.Wait()
for i, service := range needPull {
if pulledImages[i] != "" {
if pulledImages[i].ID != "" {
images[service.Image] = pulledImages[i]
}
}
@ -336,6 +334,35 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types.
}, s.stdinfo())
}
func mustPull(service types.ServiceConfig, images map[string]api.ImageSummary) (bool, error) {
if service.Provider != nil {
return false, nil
}
if service.Image == "" {
return false, nil
}
policy, duration, err := service.GetPullPolicy()
if err != nil {
return false, err
}
switch policy {
case types.PullPolicyAlways:
// force pull
return true, nil
case types.PullPolicyNever, types.PullPolicyBuild:
return false, nil
case types.PullPolicyRefresh:
img, ok := images[service.Image]
if !ok {
return true, nil
}
return time.Now().After(img.LastTagTime.Add(duration)), nil
default: // Pull if missing
_, ok := images[service.Image]
return !ok, nil
}
}
func isServiceImageToBuild(service types.ServiceConfig, services types.Services) bool {
if service.Build != nil {
return true

View File

@ -77,13 +77,19 @@ func (s *composeService) restart(ctx context.Context, projectName string, option
w := progress.ContextWriter(ctx)
return InDependencyOrder(ctx, project, func(c context.Context, service string) error {
config := project.Services[service]
err = s.waitDependencies(ctx, project, service, config.DependsOn, containers, 0)
if err != nil {
return err
}
eg, ctx := errgroup.WithContext(ctx)
for _, ctr := range containers.filter(isService(service)) {
eg.Go(func() error {
eventName := getContainerProgressName(ctr)
w.Event(progress.RestartingEvent(eventName))
timeout := utils.DurationSecondToInt(options.Timeout)
err := s.apiClient().ContainerRestart(ctx, ctr.ID, container.StopOptions{Timeout: timeout})
err = s.apiClient().ContainerRestart(ctx, ctr.ID, container.StopOptions{Timeout: timeout})
if err != nil {
return err
}

View File

@ -151,6 +151,9 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
v, ok := envResolver(project.Environment)(s)
return v, ok
}).RemoveEmpty()
if service.Environment == nil {
service.Environment = types.MappingWithEquals{}
}
service.Environment.OverrideBy(serviceOverrideEnv)
}
for k, v := range opts.Labels {

View File

@ -45,11 +45,15 @@ func (s *composeService) injectSecrets(ctx context.Context, project *types.Proje
config.Target = "/run/secrets/" + config.Target
}
env, ok := project.Environment[file.Environment]
if !ok {
return fmt.Errorf("environment variable %q required by secret %q is not set", file.Environment, file.Name)
content := file.Content
if content == "" {
env, ok := project.Environment[file.Environment]
if !ok {
return fmt.Errorf("environment variable %q required by secret %q is not set", file.Environment, file.Name)
}
content = env
}
b, err := createTar(env, types.FileReferenceConfig(config))
b, err := createTar(content, types.FileReferenceConfig(config))
if err != nil {
return err
}
@ -106,7 +110,7 @@ func createTar(env string, config types.FileReferenceConfig) (bytes.Buffer, erro
value := []byte(env)
b := bytes.Buffer{}
tarWriter := tar.NewWriter(&b)
mode := uint32(0o444)
mode := types.FileMode(0o444)
if config.Mode != nil {
mode = *config.Mode
}

View File

@ -0,0 +1,3 @@
services:
foo:
image: bar

View File

@ -0,0 +1,6 @@
name: test
services:
test:
extends:
file: common.yaml
service: foo

View File

@ -42,12 +42,21 @@ func (s *composeService) Top(ctx context.Context, projectName string, services [
if err != nil {
return err
}
summary[i] = api.ContainerProcSummary{
name := getCanonicalContainerName(ctr)
s := api.ContainerProcSummary{
ID: ctr.ID,
Name: getCanonicalContainerName(ctr),
Name: name,
Processes: topContent.Processes,
Titles: topContent.Titles,
Service: name,
}
if service, exists := ctr.Labels[api.ServiceLabel]; exists {
s.Service = service
}
if replica, exists := ctr.Labels[api.ContainerNumberLabel]; exists {
s.Replica = replica
}
summary[i] = s
return nil
})
}

View File

@ -0,0 +1,104 @@
/*
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 transform
import (
"fmt"
"gopkg.in/yaml.v3"
)
// ReplaceExtendsFile changes value for service.extends.file in input yaml stream, preserving formatting
func ReplaceExtendsFile(in []byte, service string, value string) ([]byte, error) {
var doc yaml.Node
err := yaml.Unmarshal(in, &doc)
if err != nil {
return nil, err
}
if doc.Kind != yaml.DocumentNode {
return nil, fmt.Errorf("expected document kind %v, got %v", yaml.DocumentNode, doc.Kind)
}
root := doc.Content[0]
if root.Kind != yaml.MappingNode {
return nil, fmt.Errorf("expected document root to be a mapping, got %v", root.Kind)
}
services, err := getMapping(root, "services")
if err != nil {
return nil, err
}
target, err := getMapping(services, service)
if err != nil {
return nil, err
}
extends, err := getMapping(target, "extends")
if err != nil {
return nil, err
}
file, err := getMapping(extends, "file")
if err != nil {
return nil, err
}
// we've found target `file` yaml node. Let's replace value in stream at node position
return replace(in, file.Line, file.Column, value), nil
}
func getMapping(root *yaml.Node, key string) (*yaml.Node, error) {
var node *yaml.Node
l := len(root.Content)
for i := 0; i < l; i += 2 {
k := root.Content[i]
if k.Kind != yaml.ScalarNode || k.Tag != "!!str" {
return nil, fmt.Errorf("expected mapping key to be a string, got %v %v", root.Kind, k.Tag)
}
if k.Value == key {
node = root.Content[i+1]
return node, nil
}
}
return nil, fmt.Errorf("key %v not found", key)
}
// replace changes yaml node value in stream at position, preserving content
func replace(in []byte, line int, column int, value string) []byte {
var out []byte
l := 1
pos := 0
for _, b := range in {
if b == '\n' {
l++
if l == line {
break
}
}
pos++
}
pos += column
out = append(out, in[0:pos]...)
out = append(out, []byte(value)...)
for ; pos < len(in); pos++ {
if in[pos] == '\n' {
break
}
}
out = append(out, in[pos:]...)
return out
}

View File

@ -0,0 +1,85 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package transform
import (
"reflect"
"testing"
"gotest.tools/v3/assert"
)
func TestReplace(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "simple",
in: `services:
test:
extends:
file: foo.yaml
service: foo
`,
want: `services:
test:
extends:
file: REPLACED
service: foo
`,
},
{
name: "last line",
in: `services:
test:
extends:
service: foo
file: foo.yaml
`,
want: `services:
test:
extends:
service: foo
file: REPLACED
`,
},
{
name: "last line no CR",
in: `services:
test:
extends:
service: foo
file: foo.yaml`,
want: `services:
test:
extends:
service: foo
file: REPLACED`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ReplaceExtendsFile([]byte(tt.in), "test", "REPLACED")
assert.NilError(t, err)
if !reflect.DeepEqual(got, []byte(tt.want)) {
t.Errorf("ReplaceExtendsFile() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -33,6 +33,7 @@ import (
ccli "github.com/docker/cli/cli/command/container"
pathutil "github.com/docker/compose/v2/internal/paths"
"github.com/docker/compose/v2/internal/sync"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/watch"
"github.com/docker/docker/api/types/container"
@ -80,6 +81,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
type watchRule struct {
types.Trigger
include watch.PathMatcher
ignore watch.PathMatcher
service string
}
@ -89,6 +91,15 @@ func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {
if !pathutil.IsChild(r.Path, hostPath) {
return nil
}
included, err := r.include.Matches(hostPath)
if err != nil {
logrus.Warnf("error include matching %q: %v", hostPath, err)
return nil
}
if !included {
logrus.Debugf("%s is not matching include pattern", hostPath)
return nil
}
isIgnored, err := r.ignore.Matches(hostPath)
if err != nil {
logrus.Warnf("error ignore matching %q: %v", hostPath, err)
@ -243,8 +254,19 @@ func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]
return nil, err
}
var include watch.PathMatcher
if len(trigger.Include) == 0 {
include = watch.AnyMatcher{}
} else {
include, err = watch.NewDockerPatternMatcher(trigger.Path, trigger.Include)
if err != nil {
return nil, err
}
}
rules = append(rules, watchRule{
Trigger: trigger,
include: include,
ignore: watch.NewCompositeMatcher(
dockerIgnores,
watch.EphemeralPathMatcher(),
@ -331,8 +353,11 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
for _, volume := range volumes {
if volume.Bind != nil && strings.HasPrefix(watchPath, volume.Source) {
return true
if volume.Bind != nil {
relPath, err := filepath.Rel(volume.Source, watchPath)
if err == nil && !strings.HasPrefix(relPath, "..") {
return true
}
}
}
return false
@ -522,7 +547,16 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service(s) %q after changes were detected...", services))
// restrict the build to ONLY this service, not any of its dependencies
options.Build.Services = services
imageNameToIdMap, err := s.build(ctx, project, *options.Build, nil)
var (
imageNameToIdMap map[string]string
err error
)
err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project),
func(ctx context.Context) error {
imageNameToIdMap, err = s.build(ctx, project, *options.Build, nil)
return err
})(ctx)
if err != nil {
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Build failed. Error: %v", err))
return err
@ -720,7 +754,7 @@ func (s *composeService) imageCreatedTime(ctx context.Context, project *types.Pr
return time.Now(), err
}
if len(containers) == 0 {
return time.Now(), fmt.Errorf("Could not get created time for service's image")
return time.Now(), fmt.Errorf("could not get created time for service's image")
}
img, err := s.apiClient().ImageInspect(ctx, containers[0].ImageID)

View File

@ -178,4 +178,10 @@ func TestLocalComposeRun(t *testing.T) {
assert.Assert(t, strings.Contains(res.Combined(), "backend Pulling"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "backend Pulled"), res.Combined())
})
t.Run("compose run --env-from-file", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--env-from-file", "./fixtures/run-test/run.env",
"front", "env")
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
})
}

View File

@ -21,6 +21,7 @@ import (
"testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
func TestRawEnvFile(t *testing.T) {
@ -30,3 +31,18 @@ func TestRawEnvFile(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dotenv/raw.yaml", "run", "test")
assert.Equal(t, strings.TrimSpace(res.Stdout()), "'{\"key\": \"value\"}'")
}
func TestUnusedMissingEnvFile(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "unused_dotenv")
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "up", "-d", "serviceA")
}
func TestRunEnvFile(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "run_dotenv")
res := c.RunDockerComposeCmd(t, "--project-directory", "./fixtures/env_file", "run", "serviceC", "env")
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
}

View File

@ -1,6 +1,5 @@
services:
base:
image: base
init: true
build:
context: .

View File

@ -0,0 +1,10 @@
services:
included:
image: alpine
secrets:
- my-secret
command: cat /run/secrets/my-secret
secrets:
my-secret:
environment: 'MY_SECRET'

View File

@ -1,3 +1,8 @@
include:
- path: child/compose.yaml
env_file:
- secret.env
services:
foo:
image: alpine

View File

@ -0,0 +1 @@
MY_SECRET='this-is-secret'

View File

@ -0,0 +1,14 @@
services:
serviceA:
image: nginx:latest
serviceB:
image: nginx:latest
env_file:
- /doesnotexist/.env
serviceC:
profiles: ["test"]
image: alpine
env_file: test.env

View File

@ -0,0 +1 @@
FOO=BAR

View File

@ -0,0 +1,15 @@
# Copyright 2020 Docker Compose CLI authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM alpine:latest

View File

@ -0,0 +1,3 @@
services:
foo:
image: bar

View File

@ -0,0 +1,5 @@
services:
serviceA:
image: a
volumes:
- .:/user-data

View File

@ -0,0 +1,9 @@
services:
serviceA:
build:
context: .
dockerfile: Dockerfile
serviceB:
build:
context: .
dockerfile: Dockerfile

View File

@ -0,0 +1,6 @@
include:
- common.yaml
services:
test:
image: test

View File

@ -0,0 +1,20 @@
services:
serviceA:
image: "alpine:3.12"
environment:
- AWS_ACCESS_KEY_ID=A3TX1234567890ABCDEF
- AWS_SECRET_ACCESS_KEY=aws"12345+67890/abcdefghijklm+NOPQRSTUVWXYZ+"
configs:
- myconfig
serviceB:
image: "alpine:3.12"
env_file:
- publish-sensitive.env
secrets:
- mysecret
configs:
myconfig:
file: config.txt
secrets:
mysecret:
file: secret.txt

View File

@ -0,0 +1,5 @@
services:
test:
extends:
file: common.yaml
service: foo

View File

@ -0,0 +1 @@
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw

View File

@ -0,0 +1 @@
GITHUB_TOKEN=ghp_1234567890abcdefghijklmnopqrstuvwxyz

View File

@ -1,2 +1,2 @@
FOO=bar
QUIX=
QUIX=

View File

@ -0,0 +1,3 @@
-----BEGIN DSA PRIVATE KEY-----
wxyz+ABC=
-----END DSA PRIVATE KEY-----

View File

@ -1,13 +1,13 @@
services:
with-restart:
image: alpine
image: nginx:alpine
init: true
command: tail -f /dev/null
depends_on:
nginx: {condition: service_healthy, restart: true}
no-restart:
image: alpine
image: nginx:alpine
init: true
command: tail -f /dev/null
depends_on:
@ -15,6 +15,8 @@ services:
nginx:
image: nginx:alpine
labels:
TEST: ${LABEL:-test}
healthcheck:
test: "echo | nc -w 5 localhost:80"
interval: 2s

View File

@ -1,4 +1,3 @@
version: '3.8'
services:
back:
image: alpine

View File

@ -0,0 +1 @@
FOO=BAR

View File

@ -37,4 +37,7 @@ services:
RUN mkdir -p /app/config
init: true
command: sleep infinity
volumes:
- ./dat:/app/dat
- ./data-logs:/app/data-logs
develop: *x-dev

View File

@ -0,0 +1 @@
i am a wannabe cat

View File

@ -0,0 +1 @@
[INFO] Server started successfully on port 8080

View File

@ -0,0 +1,12 @@
services:
a:
build:
dockerfile_inline: |
FROM nginx
RUN mkdir /data/
develop:
watch:
- path: .
include: A.*
target: /data/
action: sync

View File

@ -91,6 +91,12 @@ or remove sensitive data from your Compose configuration
assert.Assert(t, !strings.Contains(res.Combined(), "test/test published"), res.Combined())
})
t.Run("publish with extends", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/publish/compose-with-extends.yml",
"-p", projectName, "alpha", "publish", "test/test", "--dry-run")
assert.Assert(t, strings.Contains(res.Combined(), "test/test published"), res.Combined())
})
t.Run("publish list env variables", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-multi-env-config.yml",
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run")
@ -107,4 +113,43 @@ FOO=bar`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `BAR=baz`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `QUIX=`), res.Combined())
})
t.Run("refuse to publish with bind mount", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-bind-mount.yml",
"-p", projectName, "alpha", "publish", "test/test", "--dry-run")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `cannot publish compose file: service "serviceA" relies on bind-mount. You should use volumes`})
})
t.Run("refuse to publish with build section only", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-build-only.yml",
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "-y", "--dry-run")
res.Assert(t, icmd.Expected{ExitCode: 1})
assert.Assert(t, strings.Contains(res.Combined(), "your Compose stack cannot be published as it only contains a build section for service(s):"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "serviceA"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "serviceB"), res.Combined())
})
t.Run("refuse to publish with local include", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "-f", "./fixtures/publish/compose-local-include.yml",
"-p", projectName, "alpha", "publish", "test/test", "--dry-run")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "cannot publish compose file with local includes"})
})
t.Run("detect sensitive data", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/publish/compose-sensitive.yml",
"-p", projectName, "alpha", "publish", "test/test", "--with-env", "--dry-run")
cmd.Stdin = strings.NewReader("n\n")
res := icmd.RunCmd(cmd)
res.Assert(t, icmd.Expected{ExitCode: 0})
output := res.Combined()
assert.Assert(t, strings.Contains(output, "you are about to publish sensitive data within your OCI artifact.\n"), output)
assert.Assert(t, strings.Contains(output, "please double check that you are not leaking sensitive data"), output)
assert.Assert(t, strings.Contains(output, "AWS Client ID\n\"services.serviceA.environment.AWS_ACCESS_KEY_ID\": A3TX1234567890ABCDEF"), output)
assert.Assert(t, strings.Contains(output, "AWS Secret Key\n\"services.serviceA.environment.AWS_SECRET_ACCESS_KEY\": aws\"12345+67890/abcdefghijklm+NOPQRSTUVWXYZ+\""), output)
assert.Assert(t, strings.Contains(output, "Github authentication\n\"GITHUB_TOKEN\": ghp_1234567890abcdefghijklmnopqrstuvwxyz"), output)
assert.Assert(t, strings.Contains(output, "JSON Web Token\n\"\": eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."+
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw"), output)
assert.Assert(t, strings.Contains(output, "Private Key\n\"\": -----BEGIN DSA PRIVATE KEY-----\nwxyz+ABC=\n-----END DSA PRIVATE KEY-----"), output)
})
}

View File

@ -65,7 +65,7 @@ func TestRestart(t *testing.T) {
}
func TestRestartWithDependencies(t *testing.T) {
c := NewParallelCLI(t, WithEnv(
c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-restart-deps",
))
baseService := "nginx"
@ -80,9 +80,22 @@ func TestRestartWithDependencies(t *testing.T) {
res := c.RunDockerComposeCmd(t, "restart", baseService)
out := res.Combined()
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Restarting", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out)
assert.Assert(t, !strings.Contains(out, depNoRestart), out)
c = NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-restart-deps",
"LABEL=recreate",
))
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose-depends-on.yaml", "up", "-d")
out = res.Combined()
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Stopped", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Recreated", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Running", depNoRestart)), out)
}
func TestRestartWithProfiles(t *testing.T) {

View File

@ -41,3 +41,13 @@ func TestSecretFromEnv(t *testing.T) {
res.Assert(t, icmd.Expected{Out: "-r--r----- 1 1005 1005"})
})
}
func TestSecretFromInclude(t *testing.T) {
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "env-secret-include")
t.Run("compose run", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/env-secret/compose.yaml", "run", "included")
res.Assert(t, icmd.Expected{Out: "this-is-secret"})
})
}

View File

@ -98,7 +98,7 @@ func TestUpDependenciesNotStopped(t *testing.T) {
if exitErr.ExitCode() == -1 {
t.Fatalf("`compose up` was killed: %v", err)
}
require.EqualValues(t, 130, exitErr.ExitCode())
require.Equal(t, 130, exitErr.ExitCode())
}
RequireServiceState(t, c, "app", "exited")

View File

@ -368,3 +368,40 @@ func TestWatchMultiServices(t *testing.T) {
c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9")
}
func TestWatchIncludes(t *testing.T) {
c := NewCLI(t)
const projectName = "test_watch_includes"
defer c.cleanupWithDown(t, projectName)
tmpdir := t.TempDir()
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
CopyFile(t, filepath.Join("fixtures", "watch", "include.yaml"), composeFilePath)
cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--watch")
buffer := bytes.NewBuffer(nil)
cmd.Stdout = buffer
watch := icmd.StartCmd(cmd)
poll.WaitOn(t, func(l poll.LogT) poll.Result {
if strings.Contains(watch.Stdout(), "Attaching to ") {
return poll.Success()
}
return poll.Continue("%v", watch.Stdout())
})
require.NoError(t, os.WriteFile(filepath.Join(tmpdir, "B.test"), []byte("test"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(tmpdir, "A.test"), []byte("test"), 0o600))
poll.WaitOn(t, func(l poll.LogT) poll.Result {
cat := c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "exec", "a", "ls", "/data/")
if strings.Contains(cat.Stdout(), "A.test") {
assert.Check(t, !strings.Contains(cat.Stdout(), "B.test"))
return poll.Success()
}
return poll.Continue("%v", cat.Combined())
})
c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9")
}

View File

@ -96,6 +96,6 @@ type Pipe struct {
func (u Pipe) Confirm(message string, defaultValue bool) (bool, error) {
_, _ = fmt.Fprint(u.stdout, message)
var answer string
_, _ = fmt.Scanln(&answer)
_, _ = fmt.Fscanln(u.stdin, &answer)
return utils.StringToBool(answer), nil
}

View File

@ -17,6 +17,8 @@
package remote
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
@ -28,8 +30,10 @@ import (
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/moby/buildkit/util/gitutil"
"github.com/sirupsen/logrus"
)
const GIT_REMOTE_ENABLED = "COMPOSE_EXPERIMENTAL_GIT_REMOTE"
@ -42,19 +46,21 @@ func gitRemoteLoaderEnabled() (bool, error) {
}
return enabled, err
}
return false, nil
return true, nil
}
func NewGitRemoteLoader(offline bool) loader.ResourceLoader {
func NewGitRemoteLoader(dockerCli command.Cli, offline bool) loader.ResourceLoader {
return gitRemoteLoader{
offline: offline,
known: map[string]string{},
dockerCli: dockerCli,
offline: offline,
known: map[string]string{},
}
}
type gitRemoteLoader struct {
offline bool
known map[string]string
dockerCli command.Cli
offline bool
known map[string]string
}
func (g gitRemoteLoader) Accept(path string) bool {
@ -70,7 +76,7 @@ func (g gitRemoteLoader) Load(ctx context.Context, path string) (string, error)
return "", err
}
if !enabled {
return "", fmt.Errorf("experimental git remote resource is disabled. %q must be set", GIT_REMOTE_ENABLED)
return "", fmt.Errorf("git remote resource is disabled by %q", GIT_REMOTE_ENABLED)
}
ref, err := gitutil.ParseGitRef(path)
@ -166,7 +172,8 @@ func (g gitRemoteLoader) checkout(ctx context.Context, path string, ref *gitutil
cmd = exec.CommandContext(ctx, "git", "fetch", "--depth=1", "origin", ref.Commit)
cmd.Env = g.gitCommandEnv()
cmd.Dir = path
err = cmd.Run()
err = g.run(cmd)
if err != nil {
return err
}
@ -180,6 +187,19 @@ func (g gitRemoteLoader) checkout(ctx context.Context, path string, ref *gitutil
return nil
}
func (g gitRemoteLoader) run(cmd *exec.Cmd) error {
if logrus.IsLevelEnabled(logrus.DebugLevel) {
output, err := cmd.CombinedOutput()
scanner := bufio.NewScanner(bytes.NewBuffer(output))
for scanner.Scan() {
line := scanner.Text()
logrus.Debug(line)
}
return err
}
return cmd.Run()
}
func (g gitRemoteLoader) gitCommandEnv() []string {
env := types.NewMapping(os.Environ())
if env["GIT_TERMINAL_PROMPT"] == "" {

View File

@ -34,7 +34,10 @@ import (
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
const OCI_REMOTE_ENABLED = "COMPOSE_EXPERIMENTAL_OCI_REMOTE"
const (
OCI_REMOTE_ENABLED = "COMPOSE_EXPERIMENTAL_OCI_REMOTE"
OciPrefix = "oci://"
)
func ociRemoteLoaderEnabled() (bool, error) {
if v := os.Getenv(OCI_REMOTE_ENABLED); v != "" {
@ -44,7 +47,7 @@ func ociRemoteLoaderEnabled() (bool, error) {
}
return enabled, err
}
return false, nil
return true, nil
}
func NewOCIRemoteLoader(dockerCli command.Cli, offline bool) loader.ResourceLoader {
@ -61,10 +64,8 @@ type ociRemoteLoader struct {
known map[string]string
}
const prefix = "oci://"
func (g ociRemoteLoader) Accept(path string) bool {
return strings.HasPrefix(path, prefix)
return strings.HasPrefix(path, OciPrefix)
}
func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error) {
@ -73,7 +74,7 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
return "", err
}
if !enabled {
return "", fmt.Errorf("experimental OCI remote resource is disabled. %q must be set", OCI_REMOTE_ENABLED)
return "", fmt.Errorf("OCI remote resource is disabled by %q", OCI_REMOTE_ENABLED)
}
if g.offline {
@ -82,7 +83,7 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
local, ok := g.known[path]
if !ok {
ref, err := reference.ParseDockerRef(path[len(prefix):])
ref, err := reference.ParseDockerRef(path[len(OciPrefix):])
if err != nil {
return "", err
}
@ -104,7 +105,6 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
}
local = filepath.Join(cache, descriptor.Digest.Hex())
composeFile := filepath.Join(local, "compose.yaml")
if _, err = os.Stat(local); os.IsNotExist(err) {
var manifest v1.Manifest
err = json.Unmarshal(content, &manifest)
@ -112,16 +112,15 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
return "", err
}
err2 := g.pullComposeFiles(ctx, local, composeFile, manifest, ref, resolver)
if err2 != nil {
err = g.pullComposeFiles(ctx, local, manifest, ref, resolver)
if err != nil {
// we need to clean up the directory to be sure we won't let empty files present
_ = os.RemoveAll(local)
return "", err2
return "", err
}
}
g.known[path] = local
}
return filepath.Join(local, "compose.yaml"), nil
}
@ -129,12 +128,12 @@ func (g ociRemoteLoader) Dir(path string) string {
return g.known[path]
}
func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, composeFile string, manifest v1.Manifest, ref reference.Named, resolver *imagetools.Resolver) error {
func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, manifest v1.Manifest, ref reference.Named, resolver *imagetools.Resolver) error { //nolint:gocyclo
err := os.MkdirAll(local, 0o700)
if err != nil {
return err
}
composeFile := filepath.Join(local, "compose.yaml")
f, err := os.Create(composeFile)
if err != nil {
return err
@ -157,7 +156,15 @@ func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, com
switch layer.MediaType {
case ocipush.ComposeYAMLMediaType:
if err := writeComposeFile(layer, i, f, content); err != nil {
target := f
_, extends := layer.Annotations["com.docker.compose.extends"]
if extends {
target, err = os.Create(filepath.Join(local, layer.Annotations["com.docker.compose.file"]))
if err != nil {
return err
}
}
if err := writeComposeFile(layer, i, target, content); err != nil {
return err
}
case ocipush.ComposeEnvFileMediaType:

View File

@ -68,6 +68,15 @@ type PathMatcher interface {
MatchesEntireDir(file string) (bool, error)
}
// AnyMatcher is a PathMatcher to match any path
type AnyMatcher struct{}
func (AnyMatcher) Matches(f string) (bool, error) { return true, nil }
func (AnyMatcher) MatchesEntireDir(f string) (bool, error) { return true, nil }
var _ PathMatcher = AnyMatcher{}
// EmptyMatcher is a PathMatcher to match no path
type EmptyMatcher struct{}
func (EmptyMatcher) Matches(f string) (bool, error) { return false, nil }

View File

@ -149,7 +149,7 @@ func TestGitBranchSwitch(t *testing.T) {
f.assertEvents(path)
// Make sure there are no errors in the out stream
assert.Equal(t, "", f.out.String())
assert.Empty(t, f.out.String())
}
func TestWatchesAreRecursive(t *testing.T) {

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