Compare commits

...

104 Commits

Author SHA1 Message Date
Nicolas De Loof
f7825a56bf bump buildx to v0.25
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-20 15:16:51 +02:00
Nicolas De Loof
4cf075ea0a bump compose-go to v2.6.5
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-20 15:16:51 +02:00
Nicolas De Loof
4f491ffa98 fix panic using w shortcut on project without watch support
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-16 11:15:45 +02:00
Nicolas De Loof
ea1c26d22a restore ContainerName in images --json
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-16 09:42:07 +02:00
Nicolas De Loof
9a5fa05ad6 add (temporary) support for use_api_socket
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-13 14:43:16 +02:00
Nicolas De Loof
276c229458 move run logic inside backend
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-13 14:43:16 +02:00
dependabot[bot]
eef448dc64 build(deps): bump github.com/containerd/containerd/v2
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.1 to 2.1.2.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.1.1...v2.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 12:07:26 +02:00
dependabot[bot]
343117233b build(deps): bump google.golang.org/grpc from 1.72.2 to 1.73.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.72.2 to 1.73.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.72.2...v1.73.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-12 10:18:57 +02:00
Nicolas De Loof
f599a8cdd2 add support for extra_hosts building with bake
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-12 09:52:20 +02:00
Nicolas De Loof
63b06f5563 fix panic on failure starting plugin server
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-12 09:48:07 +02:00
Nicolas De Loof
1d34661e91 fix support for additional_contexts with service sub-dependencies
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-12 09:47:19 +02:00
Jeff Carter
0f9e6ab832 Fix the generated manifest for compose artifacts so that the empty config is not added as a layer.
Signed-off-by: Jeff Carter <jeff.carter@docker.com>
2025-06-11 16:51:51 +02:00
Nicolas De Loof
15c9651a3a restore os.Remove(metadataFile)
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-11 16:12:08 +02:00
Nicolas De Loof
4893a8b9ad don't create metadatafile, just generate a random name
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-11 15:56:26 +02:00
Nicolas De Loof
97530790fa only look for required image in bake metadata
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-11 13:46:57 +02:00
Nicolas De Loof
213c03f99a produce bake targets for all services, group for services to build
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-10 09:30:22 +02:00
Nicolas De Loof
ebd7b761f2 sanitize service name so they can be used as bake targets
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-10 09:30:22 +02:00
x0rw
ea48480d80 Restore images format list format behaviour
Signed-off-by: x0rw <mahdi.svt5@gmail.com>
2025-06-07 23:07:49 +02:00
Nicolas De Loof
8151b59288 bump golang.org/x/sync v0.15.0
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-06 16:21:29 +02:00
Guillaume Lours
ec49baca56 do not forgot to remove the bake metadata file
few DD e2e tests failed on Windows due to permission issues

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-06 14:56:40 +02:00
Nicolas De Loof
7b9ad96240 fix SIGSEGV on Enable Watch
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-06 10:07:20 +02:00
Nicolas De Loof
9b67a48c33 (refactoting) Move watch logic into a dedicated Watcher type
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-05 16:48:05 +02:00
Nicolas De Loof
0d0e12cc85 use Bake by default
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-05 16:20:44 +02:00
Guillaume Lours
92fafccfb2 add validation for required parameters of provider service when metadata are available
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-05 15:12:32 +02:00
Guillaume Lours
fee8aee8f0 save provider metadata for Docker LSP
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-05 15:12:32 +02:00
Guillaume Lours
40f5786e68 add support of metadata subcommand for provider services
This command will let Compose and external tooling know about which parameters should be passed to the Compose plugin

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-06-05 15:12:32 +02:00
Nicolas De Loof
61e44da936 debug message to help diagnose platform mismatch
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-05 11:43:13 +02:00
Nicolas De Loof
0bf7d1ea25 pull does not require env_file being resolved
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-05 11:42:35 +02:00
dependabot[bot]
80ace63dfb build(deps): bump google.golang.org/grpc from 1.72.1 to 1.72.2
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.72.1 to 1.72.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.72.1...v1.72.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 11:38:21 +02:00
Nicolas De Loof
27e90a3fdf end-to-end test
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-02 18:57:07 +02:00
Andrii Telesh
3ca75bdf55 Fix the inability to restart the Compose stack after network configuration change
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-02 18:57:07 +02:00
Nicolas De Loof
eb3074bbda include platform and creation date listing image used by running compose application
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-02 16:07:24 +02:00
Nicolas De Loof
f4fc010d6b build dependent service images when required
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-02 12:28:43 +02:00
Nicolas De Loof
693b9ef078 fix support for BUILDKIT_PROGRESS
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-02 11:03:04 +02:00
Sebastiaan van Stijn
046879a4a2 replace uses of golang.org/x/exp/(maps|slices) for stdlib
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-02 10:43:17 +02:00
Sebastiaan van Stijn
7c79b23005 pkg/bridge: fix importShadow: shadow of imported package (gocritic)
pkg/bridge/convert.go:114:3: importShadow: shadow of imported package 'user' (gocritic)
            user, err := user.Current()
            ^
    pkg/bridge/convert.go:142:51: importShadow: shadow of imported from 'github.com/docker/cli/cli/command/container' package 'cli' (gocritic)
    func LoadAdditionalResources(ctx context.Context, cli command.Cli, project *types.Project) (*types.Project, error) {
                                                      ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-02 10:43:17 +02:00
Sebastiaan van Stijn
ad4cbee498 bump github.com/docker/docker, docker/cli v28.2.2
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-02 10:39:56 +02:00
Carlos Daniel Vilaseca
60256a875c fix typo in suggestion log
Signed-off-by: Carlos Daniel Vilaseca <carlosd.vilaseca@ai.yareytech.com>
2025-05-31 19:40:32 +02:00
Nicolas De Loof
45bd60c33a resolve symlinks while making dockerfile path absolute
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-28 12:12:16 +02:00
Nicolas De Loof
cf89fd1aa1 also (re)start dependent services after watch rebuilt image
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-27 16:14:36 +02:00
Nicolas De Loof
23fef850b9 prefer use of slices.DeleteFunc
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-27 15:16:50 +02:00
Nicolas De Loof
12b73bea73 remove utils.Contains to prefer slice.ContainsFunc
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-27 15:16:50 +02:00
tongjicoder
2e71440bee refactor: use slices.Contains to simplify code
Signed-off-by: tongjicoder <tongjicoder@icloud.com>
2025-05-27 11:45:26 +02:00
Guillaume Lours
d49a68ecbf bridge - run transformer container as current user
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-27 10:35:30 +02:00
Guillaume Lours
be83f63f26 add e2e tests for bridge convert and transformers ls commands
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-27 10:35:30 +02:00
Guillaume Lours
9a9227ce64 add new bridge commands documentation
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-27 10:35:30 +02:00
Guillaume Lours
024f8ebdc5 add convert subcommand to bridge command
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-27 10:35:30 +02:00
Guillaume Lours
8c622da20b add bridge command and transformations subcommands
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-27 10:35:30 +02:00
Guillaume Lours
bbb2b76a14 bump cli-doc-tools to v0.10.0
and update the documentation to pass CI checks

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-26 16:37:45 +02:00
Guillaume Lours
e45e58b3ec bumpd buildkit v0.22.0 and buildx v0.24.0
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-23 15:03:36 +02:00
Guillaume Lours
f52af4c868 bump compose-go to v2.6.4
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-23 14:53:24 +02:00
Nicolas De Loof
a54814ff39 e2e test
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-22 14:58:53 +02:00
Nicolas De Loof
a2d7548ca9 fix up --build with additional_context dependency
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-22 14:26:34 +02:00
Nicolas De Loof
8a2cb90a39 example provider implementation
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-21 15:59:30 +02:00
Nicolas De Loof
cc50ada725 report error (re)creating container
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-21 15:40:10 +02:00
dependabot[bot]
5c74f07991 build(deps): bump github.com/containerd/containerd/v2
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.1.0...v2.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-21 12:03:59 +02:00
Guillaume Lours
7e198ee6a3 remove provenance build flag for now
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-20 18:21:05 +02:00
Guillaume Lours
0566431c64 only use attestation when building image outside the development inner loop
when building a image, by default attestation are generated and modify the image ID which trigger a container recreation on up, run command even if there isn't any changes on the image content itself

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-20 18:21:05 +02:00
Nicolas De Loof
4f6cc2a330 run ContainerStart sequentially
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-20 09:47:58 +02:00
Nicolas De Loof
2352a4a016 introduce config --lock-image-digests
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-19 13:53:01 +02:00
Guillaume Lours
1f076a3781 bump compose-go to v2.6.3
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-19 12:35:17 +02:00
Guillaume Lours
009a239510 remove convert alias from config command
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-19 12:30:09 +02:00
dependabot[bot]
3059574288 build(deps): bump google.golang.org/grpc from 1.72.0 to 1.72.1
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.72.0 to 1.72.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.72.0...v1.72.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 12:20:37 +02:00
dependabot[bot]
1229a69384 build(deps): bump github.com/containerd/containerd/v2
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.0.5 to 2.1.0.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v2.0.5...v2.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 12:07:46 +02:00
Nicolas De Loof
f2a88e02a0 ensure build dependencies are enabled
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-19 11:51:01 +02:00
Nicolas De Loof
7f9101845d report cancelled pull after another one failed
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-19 11:41:05 +02:00
Guillaume Lours
944e5e67a1 do not throw an error on build with provider services
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-16 17:47:09 +02:00
Anvar Umuraliev
23fc76a540 Fix quiet option when using COMPOSE_BAKE=1
Signed-off-by: Anvar Umuraliev <an.umuraliev@gmail.com>
2025-05-15 15:42:25 +02:00
Nicolas De Loof
053d225824 append .exe to provider name doing executable lookup on windows
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-14 17:19:35 +02:00
Nicolas De Loof
93b597ccec remove Docker EULA licensing which isn't relevant since Docker switched to a subscription model
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-14 15:02:13 +02:00
Guillaume Lours
4dcaf94c32 add support of 'debug' messages in the communication between Compose and provider binaries
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-13 18:32:03 +02:00
Nicolas De Loof
07e7619f4e set provider environment
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-13 11:31:38 +02:00
Nicolas De Loof
ed81185c5c fix provider info message
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-12 14:15:28 +02:00
Nicolas De Loof
22f8a7009f provider.options can be an array
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-12 11:49:49 +02:00
Guillaume Lours
91a0aa0265 skip push step for provider services
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-12 10:43:25 +02:00
Nicolas De Loof
7cea455c4d simplification
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-07 15:53:05 +02:00
dependabot[bot]
559a51e590 build(deps): bump golang.org/x/sys from 0.32.0 to 0.33.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/sys/compare/v0.32.0...v0.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-07 12:31:41 +02:00
dependabot[bot]
480a556bf0 build(deps): bump golang.org/x/sync from 0.13.0 to 0.14.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.13.0 to 0.14.0.
- [Commits](https://github.com/golang/sync/compare/v0.13.0...v0.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-07 12:19:47 +02:00
dependabot[bot]
6263361190 build(deps): bump github.com/moby/buildkit from 0.21.0 to 0.21.1
Bumps [github.com/moby/buildkit](https://github.com/moby/buildkit) from 0.21.0 to 0.21.1.
- [Release notes](https://github.com/moby/buildkit/releases)
- [Commits](https://github.com/moby/buildkit/compare/v0.21.0...v0.21.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-07 12:07:27 +02:00
Guillaume Lours
9ee03c3fec bump compose-go to v2.6.2
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-07 11:43:56 +02:00
Alessio Perugini
4bf18d2325 docs: regenerate
Signed-off-by: Alessio Perugini <alessio@perugini.xyz>
2025-05-06 22:33:52 +02:00
Alessio Perugini
f0f47a8aa8 e2e: add tests
Signed-off-by: Alessio Perugini <alessio@perugini.xyz>
2025-05-06 22:33:52 +02:00
Alessio Perugini
d6e3fa6d74 Fix config --variables not honoring the --format flag
When providing the --variables with --format flag, the current
implementation always printed in human readable form.
This patch correctly add the missing format in the formatter.Print
function, making the commands behave as an user would expect.

Example:
`config --variables --format json`

Signed-off-by: Alessio Perugini <alessio@perugini.xyz>
2025-05-06 22:33:52 +02:00
Nicolas De Loof
16e83f002d introduce build --check
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-06 07:54:30 +02:00
Nicolas De Loof
2dbef234dc document behavior on missing extension
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-05 14:33:35 +02:00
Nicolas De Loof
20f0ffec0b seach for provider binary in PATH
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-05 11:21:21 +02:00
Nicolas De Loof
cee6a3c660 document extensibility using service.provider
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-05 11:21:21 +02:00
Nicolas De Loof
fc8c56b407 select services implicitly declared by a service:xx build dependency
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-05 10:20:19 +02:00
Nicolas De Loof
9c998a934f fix collect image digests for service images built by bake
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-05-05 10:14:35 +02:00
Nicolas De Loof
0403f0d76d e2e test for start_interval
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-04-30 11:08:09 +02:00
dependabot[bot]
91d04a5ca9 build(deps): bump go.uber.org/mock from 0.5.1 to 0.5.2
Bumps [go.uber.org/mock](https://github.com/uber/mock) from 0.5.1 to 0.5.2.
- [Release notes](https://github.com/uber/mock/releases)
- [Changelog](https://github.com/uber-go/mock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uber/mock/compare/v0.5.1...v0.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-29 11:56:08 +02:00
Guillaume Lours
d2274ebe6c display proper event message for provider services on up and down
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-04-28 14:48:26 +02:00
Nicolas De Loof
6e35652182 fix support for remote absolute path
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-04-28 11:05:06 +02:00
Anvar Umuraliev
5bb46035cf Set --progress flag default value from env if provided
Signed-off-by: Anvar Umuraliev <an.umuraliev@gmail.com>
2025-04-25 11:56:07 +02:00
Anvar Umuraliev
f8dae06df8 Add support for COMPOSE_PROGRESS env variable
COMPOSE_PROGRESS variable supports 5 values:
- "auto" - detect console capabilities
- "tty" - use terminal capability for advanced rendering
- "plain" - dump raw events to output
- "quiet" - don't display events
- "json" - outputs a machine-readable JSON stream

Signed-off-by: Anvar Umuraliev <an.umuraliev@gmail.com>
2025-04-25 11:56:07 +02:00
Nicolas De Loof
955e4ed94e introduce networks.interface_name
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-04-24 12:06:17 +02:00
Guillaume Lours
60385e6065 bump compose-go to v2.6.1
fixing parsing of npipe as volume type

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-04-22 17:17:34 +02:00
Guillaume Lours
f5491328bb remove support of Synchronize File Shares integration with Docker Desktop
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-04-22 15:29:23 +02:00
dependabot[bot]
f46689a75e build(deps): bump github.com/containerd/containerd/v2
Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.0.4 to 2.0.5.
- [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.4...v2.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-22 15:16:18 +02:00
dependabot[bot]
8fd0c297f5 build(deps): bump google.golang.org/grpc from 1.71.1 to 1.72.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.71.1 to 1.72.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.71.1...v1.72.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-22 15:03:54 +02:00
dependabot[bot]
f3bbfdae58 build(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.1.0+incompatible to 28.1.1+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.1.0...v28.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-22 14:52:31 +02:00
dependabot[bot]
322c531a8c build(deps): bump github.com/docker/cli
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 28.1.0+incompatible to 28.1.1+incompatible.
- [Commits](https://github.com/docker/cli/compare/v28.1.0...v28.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-22 14:40:27 +02:00
skanehira
bf6b447263 fix: concurrent map writes when pulling
Signed-off-by: skanehira <sho19921005@gmail.com>
2025-04-22 10:46:05 +02:00
Simon Ser
a96c305b25 build: write --print output to stdout
stdinfo should only be used for status/progress messages: it
defaults to stderr and makes piping the output trickier. bakex
always writes `docker buildx bake --print` always uses stdout.

Signed-off-by: Simon Ser <contact@emersion.fr>
2025-04-22 10:13:07 +02:00
132 changed files with 3590 additions and 1772 deletions

View File

@@ -306,9 +306,6 @@ jobs:
echo "$sum $file" > ${file#\*}.sha256
fi
done
-
name: License
run: cp packaging/* ./bin/release/
-
name: List artifacts
run: |

View File

@@ -30,6 +30,10 @@ linters:
deny:
- pkg: io/ioutil
desc: io/ioutil package has been deprecated
- pkg: golang.org/x/exp/maps
desc: use stdlib maps package
- pkg: golang.org/x/exp/slices
desc: use stdlib slices package
- pkg: gopkg.in/yaml.v2
desc: compose-go uses yaml.v3
gocritic:

149
cmd/compose/bridge.go Normal file
View File

@@ -0,0 +1,149 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"io"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/bridge"
)
func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "bridge CMD [OPTIONS]",
Short: "Convert compose files into another model",
TraverseChildren: true,
}
cmd.AddCommand(
convertCommand(p, dockerCli),
transformersCommand(dockerCli),
)
return cmd
}
func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
convertOpts := bridge.ConvertOptions{}
cmd := &cobra.Command{
Use: "convert",
Short: "Convert compose files to Kubernetes manifests, Helm charts, or another model",
RunE: Adapt(func(ctx context.Context, args []string) error {
return runConvert(ctx, dockerCli, p, convertOpts)
}),
}
flags := cmd.Flags()
flags.StringVarP(&convertOpts.Output, "output", "o", "out", "The output directory for the Kubernetes resources")
flags.StringArrayVarP(&convertOpts.Transformations, "transformation", "t", nil, "Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)")
flags.StringVar(&convertOpts.Templates, "templates", "", "Directory containing transformation templates")
return cmd
}
func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
project, _, err := p.ToProject(ctx, dockerCli, nil)
if err != nil {
return err
}
return bridge.Convert(ctx, dockerCli, project, opts)
}
func transformersCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "transformations CMD [OPTIONS]",
Short: "Manage transformation images",
}
cmd.AddCommand(
listTransformersCommand(dockerCli),
createTransformerCommand(dockerCli),
)
return cmd
}
func listTransformersCommand(dockerCli command.Cli) *cobra.Command {
options := lsOptions{}
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List available transformations",
RunE: Adapt(func(ctx context.Context, args []string) error {
transformers, err := bridge.ListTransformers(ctx, dockerCli)
if err != nil {
return err
}
return displayTransformer(dockerCli, transformers, options)
}),
}
cmd.Flags().StringVar(&options.Format, "format", "table", "Format the output. Values: [table | json]")
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display transformer names")
return cmd
}
func displayTransformer(dockerCli command.Cli, transformers []image.Summary, options lsOptions) error {
if options.Quiet {
for _, t := range transformers {
if len(t.RepoTags) > 0 {
_, _ = fmt.Fprintln(dockerCli.Out(), t.RepoTags[0])
} else {
_, _ = fmt.Fprintln(dockerCli.Out(), t.ID)
}
}
return nil
}
return formatter.Print(transformers, options.Format, dockerCli.Out(),
func(w io.Writer) {
for _, img := range transformers {
id := stringid.TruncateID(img.ID)
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
repo, tag := "<none>", "<none>"
if len(img.RepoTags) > 0 {
ref, err := reference.ParseDockerRef(img.RepoTags[0])
if err == nil {
// ParseDockerRef will reject a local image ID
repo = reference.FamiliarName(ref)
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
}
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", id, repo, tag, size)
}
},
"IMAGE ID", "REPO", "TAGS", "SIZE")
}
func createTransformerCommand(dockerCli command.Cli) *cobra.Command {
var opts bridge.CreateTransformerOptions
cmd := &cobra.Command{
Use: "create [OPTION] PATH",
Short: "Create a new transformation",
RunE: Adapt(func(ctx context.Context, args []string) error {
opts.Dest = args[0]
return bridge.CreateTransformer(ctx, dockerCli, opts)
}),
}
cmd.Flags().StringVarP(&opts.From, "from", "f", "", "Existing transformation to copy (default: docker/compose-bridge-kubernetes)")
return cmd
}

View File

@@ -27,8 +27,6 @@ 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"
"github.com/docker/compose/v2/pkg/api"
@@ -36,16 +34,18 @@ import (
type buildOptions struct {
*ProjectOptions
quiet bool
pull bool
push bool
args []string
noCache bool
memory cliopts.MemBytes
ssh string
builder string
deps bool
print bool
quiet bool
pull bool
push bool
args []string
noCache bool
memory cliopts.MemBytes
ssh string
builder string
deps bool
print bool
check bool
provenance bool
}
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
@@ -69,19 +69,22 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
if uiMode == ui.ModeJSON {
uiMode = "rawjson"
}
return api.BuildOptions{
Pull: opts.pull,
Push: opts.push,
Progress: uiMode,
Args: types.NewMappingWithEquals(opts.args),
NoCache: opts.noCache,
Quiet: opts.quiet,
Services: services,
Deps: opts.deps,
Memory: int64(opts.memory),
Print: opts.print,
SSHs: SSHKeys,
Builder: builderName,
Pull: opts.pull,
Push: opts.push,
Progress: uiMode,
Args: types.NewMappingWithEquals(opts.args),
NoCache: opts.noCache,
Quiet: opts.quiet,
Services: services,
Deps: opts.deps,
Memory: int64(opts.memory),
Print: opts.print,
Check: opts.check,
SSHs: SSHKeys,
Builder: builderName,
Provenance: opts.provenance,
}, nil
}
@@ -133,47 +136,29 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
flags.Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
flags.MarkHidden("no-rm") //nolint:errcheck
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.StringVar(&p.Progress, "progress", "", 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")
flags.BoolVar(&opts.check, "check", false, "Check build configuration")
return cmd
}
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
services = addBuildDependencies(services, project)
if err := applyPlatforms(project, false); err != nil {
return err
}
apiBuildOptions, err := opts.toAPIBuildOptions(services)
apiBuildOptions.Provenance = true
if err != nil {
return err
}
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()
}

View File

@@ -47,7 +47,6 @@ import (
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/remote"
"github.com/docker/compose/v2/pkg/utils"
buildkit "github.com/moby/buildkit/util/progress/progressui"
"github.com/morikuni/aec"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -69,6 +68,8 @@ const (
ComposeEnvFiles = "COMPOSE_ENV_FILES"
// ComposeMenu defines if the navigation menu should be rendered. Can be also set via --menu
ComposeMenu = "COMPOSE_MENU"
// ComposeProgress defines type of progress output, if --progress isn't used
ComposeProgress = "COMPOSE_PROGRESS"
)
// rawEnv load a dot env file using docker/cli key=value parser, without attempt to interpolate or evaluate values
@@ -228,7 +229,7 @@ func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
f.StringVar(&o.Progress, "progress", string(buildkit.AutoMode), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
f.StringVar(&o.Progress, "progress", os.Getenv(ComposeProgress), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
f.BoolVar(&o.All, "all-resources", false, "Include all resources, even those not used by services")
_ = f.MarkHidden("workdir")
}
@@ -506,8 +507,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
}
switch opts.Progress {
case ui.ModeAuto:
ui.Mode = ui.ModeAuto
case "", ui.ModeAuto:
if ansi == "never" {
ui.Mode = ui.ModePlain
}
@@ -635,6 +635,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
watchCommand(&opts, dockerCli, backend),
publishCommand(&opts, dockerCli, backend),
alphaCommand(&opts, dockerCli, backend),
bridgeCommand(&opts, dockerCli),
)
c.Flags().SetInterspersed(false)

View File

@@ -56,6 +56,7 @@ type configOptions struct {
noConsistency bool
variables bool
environment bool
lockImageDigests bool
}
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
@@ -85,9 +86,8 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
ProjectOptions: p,
}
cmd := &cobra.Command{
Aliases: []string{"convert"}, // for backward compatibility with Cloud integrations
Use: "config [OPTIONS] [SERVICE...]",
Short: "Parse, resolve and render compose file in canonical format",
Use: "config [OPTIONS] [SERVICE...]",
Short: "Parse, resolve and render compose file in canonical format",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet {
devnull, err := os.Open(os.DevNull)
@@ -99,6 +99,9 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
if p.Compatibility {
opts.noNormalize = true
}
if opts.lockImageDigests {
opts.resolveImageDigests = true
}
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
@@ -124,13 +127,17 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
return runEnvironment(ctx, dockerCli, opts, args)
}
if opts.Format == "" {
opts.Format = "yaml"
}
return runConfig(ctx, dockerCli, opts, args)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
flags := cmd.Flags()
flags.StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
flags.StringVar(&opts.Format, "format", "", "Format the output. Values: [yaml | json]")
flags.BoolVar(&opts.resolveImageDigests, "resolve-image-digests", false, "Pin image tags to digests")
flags.BoolVar(&opts.lockImageDigests, "lock-image-digests", false, "Produces an override file with image digests")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only validate the configuration, don't print anything")
flags.BoolVar(&opts.noInterpolate, "no-interpolate", false, "Don't interpolate environment variables")
flags.BoolVar(&opts.noNormalize, "no-normalize", false, "Don't normalize compose model")
@@ -206,6 +213,10 @@ func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts confi
}
}
if opts.lockImageDigests {
project = imagesOnly(project)
}
var content []byte
switch opts.Format {
case "json":
@@ -221,6 +232,18 @@ func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts confi
return content, nil
}
// imagesOnly return project with all attributes removed but service.images
func imagesOnly(project *types.Project) *types.Project {
digests := types.Services{}
for name, config := range project.Services {
digests[name] = types.ServiceConfig{
Image: config.Image,
}
}
project = &types.Project{Services: digests}
return project
}
func runConfigNoInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
// we can't use ToProject, so the model we render here is only partially resolved
model, err := opts.ToModel(ctx, dockerCli, services)
@@ -235,6 +258,23 @@ func runConfigNoInterpolate(ctx context.Context, dockerCli command.Cli, opts con
}
}
if opts.lockImageDigests {
for key, e := range model {
if key != "services" {
delete(model, key)
} else {
for _, s := range e.(map[string]any) {
service := s.(map[string]any)
for key := range service {
if key != "image" {
delete(service, key)
}
}
}
}
}
}
return formatModel(model, opts.Format)
}
@@ -408,7 +448,16 @@ func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions
variables := template.ExtractVariables(model, template.DefaultPattern)
return formatter.Print(variables, "", dockerCli.Out(), func(w io.Writer) {
if opts.Format == "yaml" {
result, err := yaml.Marshal(variables)
if err != nil {
return err
}
fmt.Print(string(result))
return nil
}
return formatter.Print(variables, opts.Format, dockerCli.Out(), func(w io.Writer) {
for name, variable := range variables {
_, _ = fmt.Fprintf(w, "%s\t%t\t%s\t%s\n", name, variable.Required, variable.DefaultValue, variable.PresenceValue)
}

View File

@@ -20,9 +20,12 @@ import (
"context"
"fmt"
"io"
"sort"
"maps"
"slices"
"strings"
"time"
"github.com/containerd/platforms"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
@@ -30,7 +33,6 @@ import (
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
)
type imageOptions struct {
@@ -76,7 +78,7 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
if i := strings.IndexRune(img.ID, ':'); i >= 0 {
id = id[i+1:]
}
if !utils.StringContains(ids, id) {
if !slices.Contains(ids, id) {
ids = append(ids, id)
}
}
@@ -85,14 +87,42 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
}
return nil
}
if opts.Format == "json" {
sort.Slice(images, func(i, j int) bool {
return images[i].ContainerName < images[j].ContainerName
})
type img struct {
ID string `json:"ID"`
ContainerName string `json:"ContainerName"`
Repository string `json:"Repository"`
Tag string `json:"Tag"`
Platform string `json:"Platform"`
Size int64 `json:"Size"`
LastTagTime time.Time `json:"LastTagTime"`
}
// Convert map to slice
var imageList []img
for ctr, i := range images {
imageList = append(imageList, img{
ContainerName: ctr,
ID: i.ID,
Repository: i.Repository,
Tag: i.Tag,
Platform: platforms.Format(i.Platform),
Size: i.Size,
LastTagTime: i.LastTagTime,
})
}
json, err := formatter.ToJSON(imageList, "", "")
if err != nil {
return err
}
_, err = fmt.Fprintln(dockerCli.Out(), json)
return err
}
return formatter.Print(images, opts.Format, dockerCli.Out(),
func(w io.Writer) {
for _, img := range images {
for _, container := range slices.Sorted(maps.Keys(images)) {
img := images[container]
id := stringid.TruncateID(img.ID)
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
repo := img.Repository
@@ -103,8 +133,10 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
if tag == "" {
tag = "<none>"
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", img.ContainerName, repo, tag, id, size)
created := units.HumanDuration(time.Now().UTC().Sub(img.LastTagTime)) + " ago"
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
container, repo, tag, platforms.Format(img.Platform), id, size, created)
}
},
"CONTAINER", "REPOSITORY", "TAG", "IMAGE ID", "SIZE")
"CONTAINER", "REPOSITORY", "TAG", "PLATFORM", "IMAGE ID", "SIZE", "CREATED")
}

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"io"
"os"
"slices"
"sort"
"strings"
"text/tabwriter"
@@ -32,7 +33,6 @@ import (
"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"
)
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
@@ -44,7 +44,7 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
// default platform only applies if the service doesn't specify
if defaultPlatform != "" && service.Platform == "" {
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, defaultPlatform) {
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, defaultPlatform) {
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, defaultPlatform)
}
service.Platform = defaultPlatform
@@ -52,7 +52,7 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
if service.Platform != "" {
if len(service.Build.Platforms) > 0 {
if !utils.StringContains(service.Build.Platforms, service.Platform) {
if !slices.Contains(service.Build.Platforms, service.Platform) {
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
}
}

View File

@@ -20,12 +20,12 @@ import (
"context"
"errors"
"fmt"
"slices"
"sort"
"strings"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/cli/cli/command"
cliformatter "github.com/docker/cli/cli/command/formatter"
@@ -101,7 +101,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
names := project.ServiceNames()
if len(services) > 0 {
for _, service := range services {
if !utils.StringContains(names, service) {
if !slices.Contains(names, service) {
return fmt.Errorf("no such service: %s", service)
}
}
@@ -139,7 +139,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
services := []string{}
for _, c := range containers {
s := c.Service
if !utils.StringContains(services, s) {
if !slices.Contains(services, s) {
services = append(services, s)
}
}

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"os"
"github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/morikuni/aec"
@@ -97,7 +98,7 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
}
func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error {
project, _, err := opts.ToProject(ctx, dockerCli, services)
project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}

View File

@@ -271,22 +271,6 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
return err
}
err = progress.Run(ctx, func(ctx context.Context) error {
var buildForDeps *api.BuildOptions
if !createOpts.noBuild {
// allow dependencies needing build to be implicitly selected
bo, err := buildOpts.toAPIBuildOptions(nil)
if err != nil {
return err
}
buildForDeps = &bo
}
return startDependencies(ctx, backend, *project, buildForDeps, options)
}, dockerCli.Err())
if err != nil {
return err
}
labels := types.Labels{}
for _, s := range options.labels {
parts := strings.SplitN(s, "=", 2)
@@ -298,9 +282,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
var buildForRun *api.BuildOptions
if !createOpts.noBuild {
// dependencies have already been started above, so only the service
// being run might need to be built at this point
bo, err := buildOpts.toAPIBuildOptions([]string{options.Service})
bo, err := buildOpts.toAPIBuildOptions(project.ServiceNames())
if err != nil {
return err
}
@@ -314,7 +296,12 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
// start container and attach to container streams
runOpts := api.RunOptions{
Build: buildForRun,
CreateOptions: api.CreateOptions{
Build: buildForRun,
RemoveOrphans: options.removeOrphans,
IgnoreOrphans: options.ignoreOrphans,
QuietPull: options.quietPull,
},
Name: options.name,
Service: options.Service,
Command: options.Command,
@@ -324,15 +311,14 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
Interactive: options.interactive,
WorkingDir: options.workdir,
User: options.user,
CapAdd: options.capAdd.GetAll(),
CapDrop: options.capDrop.GetAll(),
CapAdd: options.capAdd.GetSlice(),
CapDrop: options.capDrop.GetSlice(),
Environment: environment.Values(),
Entrypoint: options.entrypointCmd,
Labels: labels,
UseNetworkAliases: options.useAliases,
NoDeps: options.noDeps,
Index: 0,
QuietPull: options.quietPull,
}
for name, service := range project.Services {
@@ -352,34 +338,3 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
}
return err
}
func startDependencies(ctx context.Context, backend api.Service, project types.Project, buildOpts *api.BuildOptions, options runOptions) error {
dependencies := types.Services{}
var requestedService types.ServiceConfig
for name, service := range project.Services {
if name != options.Service {
dependencies[name] = service
} else {
requestedService = service
}
}
project.Services = dependencies
project.DisabledServices[options.Service] = requestedService
err := backend.Create(ctx, &project, api.CreateOptions{
Build: buildOpts,
IgnoreOrphans: options.ignoreOrphans,
RemoveOrphans: options.removeOrphans,
QuietPull: options.quietPull,
})
if err != nil {
return err
}
if len(dependencies) > 0 {
return backend.Start(ctx, project.Name, api.StartOptions{
Project: &project,
})
}
return nil
}

View File

@@ -19,14 +19,13 @@ package compose
import (
"context"
"fmt"
"maps"
"slices"
"strconv"
"strings"
"github.com/docker/cli/cli/command"
"github.com/compose-spec/compose-go/v2/types"
"golang.org/x/exp/maps"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra"
)
@@ -60,7 +59,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
}
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
services := maps.Keys(serviceReplicaTuples)
services := slices.Sorted(maps.Keys(serviceReplicaTuples))
project, _, err := opts.ToProject(ctx, dockerCli, services)
if err != nil {
return err

View File

@@ -260,6 +260,8 @@ func runUp(
if err != nil {
return err
}
bo.Services = services
bo.Deps = !upOptions.noDeps
build = &bo
}

View File

@@ -117,9 +117,10 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
}
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false)
return backend.Watch(ctx, project, services, api.WatchOptions{
Build: &build,
LogTo: consumer,
Prune: watchOpts.prune,
return backend.Watch(ctx, project, api.WatchOptions{
Build: &build,
LogTo: consumer,
Prune: watchOpts.prune,
Services: services,
})
}

View File

@@ -22,6 +22,7 @@ import (
"fmt"
"math"
"os"
"reflect"
"syscall"
"time"
@@ -29,9 +30,7 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/watch"
"github.com/eiannone/keyboard"
"github.com/hashicorp/go-multierror"
"github.com/skratchdot/open-golang/open"
)
@@ -71,26 +70,14 @@ func (ke *KeyboardError) error() string {
}
type KeyboardWatch struct {
Watcher watch.Notify
Watching bool
WatchFn func(ctx context.Context, doneCh chan bool, project *types.Project, services []string, options api.WatchOptions) error
Ctx context.Context
Cancel context.CancelFunc
Watching bool
Watcher Toggle
IsConfigured bool
}
func (kw *KeyboardWatch) isWatching() bool {
return kw.Watching
}
func (kw *KeyboardWatch) switchWatching() {
kw.Watching = !kw.Watching
}
func (kw *KeyboardWatch) newContext(ctx context.Context) context.CancelFunc {
ctx, cancel := context.WithCancel(ctx)
kw.Ctx = ctx
kw.Cancel = cancel
return cancel
type Toggle interface {
Start(context.Context) error
Stop() error
}
type KEYBOARD_LOG_LEVEL int
@@ -105,36 +92,25 @@ type LogKeyboard struct {
kError KeyboardError
Watch KeyboardWatch
IsDockerDesktopActive bool
IsWatchConfigured bool
logLevel KEYBOARD_LOG_LEVEL
signalChannel chan<- os.Signal
}
var (
KeyboardManager *LogKeyboard
eg multierror.Group
)
// FIXME(ndeloof) we should avoid use of such a global reference. see use in logConsumer
var KeyboardManager *LogKeyboard
func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured bool,
sc chan<- os.Signal,
watchFn func(ctx context.Context,
doneCh chan bool,
project *types.Project,
services []string,
options api.WatchOptions,
) error,
) {
km := LogKeyboard{}
km.IsDockerDesktopActive = isDockerDesktopActive
km.IsWatchConfigured = isWatchConfigured
km.logLevel = INFO
km.Watch.Watching = false
km.Watch.WatchFn = watchFn
km.signalChannel = sc
KeyboardManager = &km
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal, w bool, watcher Toggle) *LogKeyboard {
KeyboardManager = &LogKeyboard{
Watch: KeyboardWatch{
Watching: w,
Watcher: watcher,
IsConfigured: !reflect.ValueOf(watcher).IsNil(),
},
IsDockerDesktopActive: isDockerDesktopActive,
logLevel: INFO,
signalChannel: sc,
}
return KeyboardManager
}
func (lk *LogKeyboard) ClearKeyboardInfo() {
@@ -233,48 +209,51 @@ func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Pro
if !lk.IsDockerDesktopActive {
return
}
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
func(ctx context.Context) error {
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")
lk.keyboardError("View", err)
}
return err
}),
)
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
func(ctx context.Context) error {
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")
lk.keyboardError("View", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
if !lk.IsDockerDesktopActive {
return
}
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
func(ctx context.Context) error {
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")
lk.keyboardError("View Config", err)
}
return err
}),
)
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
func(ctx context.Context) error {
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")
lk.keyboardError("View Config", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Project) {
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
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")
lk.keyboardError("Watch Docs", err)
}
return err
}),
)
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
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")
lk.keyboardError("Watch Docs", err)
}
return err
})()
}()
}
func (lk *LogKeyboard) keyboardError(prefix string, err error) {
@@ -288,58 +267,54 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
}()
}
func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
if !lk.IsWatchConfigured {
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
if !lk.Watch.IsConfigured {
return
}
lk.Watch.switchWatching()
if !lk.Watch.isWatching() {
lk.Watch.Cancel()
if lk.Watch.Watching {
err := lk.Watch.Watcher.Stop()
if err != nil {
options.Start.Attach.Err(api.WatchLogger, err.Error())
} else {
lk.Watch.Watching = false
}
} else {
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")
lk.keyboardError("Watch", err)
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := lk.Watch.Watcher.Start(ctx)
if err != nil {
options.Start.Attach.Err(api.WatchLogger, err.Error())
} else {
lk.Watch.Watching = true
}
return err
}
lk.Watch.newContext(ctx)
buildOpts := *options.Create.Build
buildOpts.Quiet = true
err := lk.Watch.WatchFn(lk.Watch.Ctx, doneCh, project, options.Start.Services, api.WatchOptions{
Build: &buildOpts,
LogTo: options.Start.Attach,
})
if err != nil {
lk.Watch.switchWatching()
options.Start.Attach.Err(api.WatchLogger, err.Error())
}
return err
}))
})()
}()
}
}
func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
switch kRune := event.Rune; kRune {
case 'v':
lk.openDockerDesktop(ctx, project)
case 'w':
if !lk.IsWatchConfigured {
if !lk.Watch.IsConfigured {
// we try to open watch docs if DD is installed
if lk.IsDockerDesktopActive {
lk.openDDWatchDocs(ctx, project)
}
// either way we mark menu/watch as an error
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
lk.keyboardError("Watch", err)
return err
}))
return
go func() {
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
func(ctx context.Context) error {
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
lk.keyboardError("Watch", err)
return err
})()
}()
}
lk.StartWatch(ctx, doneCh, project, options)
lk.ToggleWatch(ctx, options)
case 'o':
lk.openDDComposeUI(ctx, project)
}
@@ -350,10 +325,6 @@ func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Cont
ShowCursor()
lk.logLevel = NONE
if lk.Watch.Watching && lk.Watch.Cancel != nil {
lk.Watch.Cancel()
_ = eg.Wait().ErrorOrNil() // Need to print this ?
}
// will notify main thread to kill and will handle gracefully
lk.signalChannel <- syscall.SIGINT
case keyboard.KeyEnter:

142
docs/examples/provider.go Normal file
View File

@@ -0,0 +1,142 @@
/*
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 main
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func main() {
cmd := &cobra.Command{
Short: "Compose Provider Example",
Use: "demo",
}
cmd.AddCommand(composeCommand())
err := cmd.Execute()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func composeCommand() *cobra.Command {
c := &cobra.Command{
Use: "compose EVENT",
TraverseChildren: true,
}
c.PersistentFlags().String("project-name", "", "compose project name") // unused
upCmd := &cobra.Command{
Use: "up",
Run: up,
Args: cobra.ExactArgs(1),
}
upCmd.Flags().String("type", "", "Database type (mysql, postgres, etc.)")
_ = upCmd.MarkFlagRequired("type")
upCmd.Flags().Int("size", 10, "Database size in GB")
upCmd.Flags().String("name", "", "Name of the database to be created")
_ = upCmd.MarkFlagRequired("name")
downCmd := &cobra.Command{
Use: "down",
Run: down,
Args: cobra.ExactArgs(1),
}
downCmd.Flags().String("name", "", "Name of the database to be deleted")
_ = downCmd.MarkFlagRequired("name")
c.AddCommand(upCmd, downCmd)
c.AddCommand(metadataCommand(upCmd, downCmd))
return c
}
const lineSeparator = "\n"
func up(_ *cobra.Command, args []string) {
servicename := args[0]
fmt.Printf(`{ "type": "debug", "message": "Starting %s" }%s`, servicename, lineSeparator)
for i := 0; i < 100; i += 10 {
time.Sleep(1 * time.Second)
fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i, lineSeparator)
}
fmt.Printf(`{ "type": "setenv", "message": "URL=https://magic.cloud/%s" }%s`, servicename, lineSeparator)
}
func down(_ *cobra.Command, _ []string) {
fmt.Printf(`{ "type": "error", "message": "Permission error" }%s`, lineSeparator)
}
func metadataCommand(upCmd, downCmd *cobra.Command) *cobra.Command {
return &cobra.Command{
Use: "metadata",
Run: func(cmd *cobra.Command, _ []string) {
metadata(upCmd, downCmd)
},
Args: cobra.NoArgs,
}
}
func metadata(upCmd, downCmd *cobra.Command) {
metadata := ProviderMetadata{}
metadata.Description = "Manage services on AwesomeCloud"
metadata.Up = commandParameters(upCmd)
metadata.Down = commandParameters(downCmd)
jsonMetadata, err := json.Marshal(metadata)
if err != nil {
panic(err)
}
fmt.Println(string(jsonMetadata))
}
func commandParameters(cmd *cobra.Command) CommandMetadata {
cmdMetadata := CommandMetadata{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
_, isRequired := f.Annotations[cobra.BashCompOneRequiredFlag]
cmdMetadata.Parameters = append(cmdMetadata.Parameters, Metadata{
Name: f.Name,
Description: f.Usage,
Required: isRequired,
Type: f.Value.Type(),
Default: f.DefValue,
})
})
return cmdMetadata
}
type ProviderMetadata struct {
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
}
type CommandMetadata struct {
Parameters []Metadata `json:"parameters"`
}
type Metadata struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}

176
docs/extension.md Normal file
View File

@@ -0,0 +1,176 @@
# About
The Compose application model defines `service` as an abstraction for a computing unit managing (a subset of)
application needs, which can interact with other service by relying on network(s). Docker Compose is designed
to use the Docker Engine ("Moby") API to manage services as containers, but the abstraction _could_ also cover
many other runtimes, typically cloud services or services natively provided by host.
The Compose extensibility model has been designed to extend the `service` support to runtimes accessible through
third-party tooling.
# Architecture
Compose extensibility relies on the `provider` attribute to select the actual binary responsible for managing
the resource(s) needed to run a service.
```yaml
database:
provider:
type: awesomecloud
options:
type: mysql
size: 256
name: myAwesomeCloudDB
```
`provider.type` tells Compose the binary to run, which can be either:
- Another Docker CLI plugin (typically, `model` to run `docker-model`)
- An executable in user's `PATH`
If `provider.type` doesn't resolve into any of those, Compose will report an error and interrupt the `up` command.
To be a valid Compose extension, provider command *MUST* accept a `compose` command (which can be hidden)
with subcommands `up` and `down`.
## Up lifecycle
To execute an application's `up` lifecycle, Compose executes the provider's `compose up` command, passing
the project name, service name, and additional options. The `provider.options` are translated
into command line flags. For example:
```console
awesomecloud compose --project-name <NAME> up --type=mysql --size=256 "database"
```
> __Note:__ `project-name` _should_ be used by the provider to tag resources
> set for project, so that later execution with `down` subcommand releases
> all allocated resources set for the project.
## Communication with Compose
Providers can interact with Compose using `stdout` as a channel, sending JSON line delimited messages.
JSON messages MUST include a `type` and a `message` attribute.
```json
{ "type": "info", "message": "preparing mysql ..." }
```
`type` can be either:
- `info`: Reports status updates to the user. Compose will render message as the service state in the progress UI
- `error`: Let's the user know something went wrong with details about the error. Compose will render the message as the reason for the service failure.
- `setenv`: Let's the plugin tell Compose how dependent services can access the created resource. See next section for further details.
- `debug`: Those messages could help debugging the provider, but are not rendered to the user by default. They are rendered when Compose is started with `--verbose` flag.
```mermaid
sequenceDiagram
Shell->>Compose: docker compose up
Compose->>Provider: compose up --project-name=xx --foo=bar "database"
Provider--)Compose: json { "info": "pulling 25%" }
Compose-)Shell: pulling 25%
Provider--)Compose: json { "info": "pulling 50%" }
Compose-)Shell: pulling 50%
Provider--)Compose: json { "info": "pulling 75%" }
Compose-)Shell: pulling 75%
Provider--)Compose: json { "setenv": "URL=http://cloud.com/abcd:1234" }
Compose-)Compose: set DATABASE_URL
Provider-)Compose: EOF (command complete) exit 0
Compose-)Shell: service started
```
## Connection to a service managed by a provider
A service in the Compose application can declare dependency on a service managed by an external provider:
```yaml
services:
app:
image: myapp
depends_on:
- database
database:
provider:
type: awesomecloud
```
When the provider command sends a `setenv` JSON message, Compose injects the specified variable into any dependent service,
automatically prefixing it with the service name. For example, if `awesomecloud compose up` returns:
```json
{"type": "setenv", "message": "URL=https://awesomecloud.com/db:1234"}
```
Then the `app` service, which depends on the service managed by the provider, will receive a `DATABASE_URL` environment variable injected
into its runtime environment.
> __Note:__ The `compose up` provider command _MUST_ be idempotent. If resource is already running, the command _MUST_ set
> the same environment variables to ensure consistent configuration of dependent services.
## Down lifecycle
`down` lifecycle is equivalent to `up` with the `<provider> compose --project-name <NAME> down <SERVICE>` command.
The provider is responsible for releasing all resources associated with the service.
## Provide metadata about options
Compose extensions *MAY* optionally implement a `metadata` subcommand to provide information about the parameters accepted by the `up` and `down` commands.
The `metadata` subcommand takes no parameters and returns a JSON structure on the `stdout` channel that describes the parameters accepted by both the `up` and `down` commands, including whether each parameter is mandatory or optional.
```console
awesomecloud compose metadata
```
The expected JSON output format is:
```json
{
"description": "Manage services on AwesomeCloud",
"up": {
"parameters": [
{
"name": "type",
"description": "Database type (mysql, postgres, etc.)",
"required": true,
"type": "string"
},
{
"name": "size",
"description": "Database size in GB",
"required": false,
"type": "integer",
"default": "10"
},
{
"name": "name",
"description": "Name of the database to be created",
"required": true,
"type": "string"
}
]
},
"down": {
"parameters": [
{
"name": "name",
"description": "Name of the database to be removed",
"required": true,
"type": "string"
}
]
}
}
```
The top elements are:
- `description`: Human-readable description of the provider
- `up`: Object describing the parameters accepted by the `up` command
- `down`: Object describing the parameters accepted by the `down` command
And for each command parameter, you should include the following properties:
- `name`: The parameter name (without `--` prefix)
- `description`: Human-readable description of the parameter
- `required`: Boolean indicating if the parameter is mandatory
- `type`: Parameter type (`string`, `integer`, `boolean`, etc.)
- `default`: Default value (optional, only for non-required parameters)
- `enum`: List of possible values supported by the parameter separated by `,` (optional, only for parameters with a limited set of values)
This metadata allows Compose and other tools to understand the provider's interface and provide better user experience, such as validation, auto-completion, and documentation generation.
## Examples
See [example](examples/provider.go) for illustration on implementing this API in a command line

View File

@@ -12,6 +12,7 @@ Define and run multi-container applications with Docker
| Name | Description |
|:--------------------------------|:----------------------------------------------------------------------------------------|
| [`attach`](compose_attach.md) | Attach local standard input, output, and error streams to a service's running container |
| [`bridge`](compose_bridge.md) | Convert compose files into another model |
| [`build`](compose_build.md) | Build or rebuild services |
| [`commit`](compose_commit.md) | Create a new image from a service container's changes |
| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format |
@@ -58,7 +59,7 @@ Define and run multi-container applications with Docker
| `-f`, `--file` | `stringArray` | | Compose configuration files |
| `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited |
| `--profile` | `stringArray` | | Specify a profile to enable |
| `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, json, quiet) |
| `--progress` | `string` | | Set type of progress output (auto, tty, plain, json, quiet) |
| `--project-directory` | `string` | | Specify an alternate working directory<br>(default: the path of the, first specified, Compose file) |
| `-p`, `--project-name` | `string` | | Project name |

View File

@@ -0,0 +1,22 @@
# docker compose bridge
<!---MARKER_GEN_START-->
Convert compose files into another model
### Subcommands
| Name | Description |
|:-------------------------------------------------------|:-----------------------------------------------------------------------------|
| [`convert`](compose_bridge_convert.md) | Convert compose files to Kubernetes manifests, Helm charts, or another model |
| [`transformations`](compose_bridge_transformations.md) | Manage transformation images |
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
<!---MARKER_GEN_END-->

View File

@@ -0,0 +1,17 @@
# docker compose bridge convert
<!---MARKER_GEN_START-->
Convert compose files to Kubernetes manifests, Helm charts, or another model
### Options
| Name | Type | Default | Description |
|:-------------------------|:--------------|:--------|:-------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-o`, `--output` | `string` | `out` | The output directory for the Kubernetes resources |
| `--templates` | `string` | | Directory containing transformation templates |
| `-t`, `--transformation` | `stringArray` | | Transformation to apply to compose model (default: docker/compose-bridge-kubernetes) |
<!---MARKER_GEN_END-->

View File

@@ -0,0 +1,22 @@
# docker compose bridge transformations
<!---MARKER_GEN_START-->
Manage transformation images
### Subcommands
| Name | Description |
|:-----------------------------------------------------|:-------------------------------|
| [`create`](compose_bridge_transformations_create.md) | Create a new transformation |
| [`list`](compose_bridge_transformations_list.md) | List available transformations |
### Options
| Name | Type | Default | Description |
|:------------|:-------|:--------|:--------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
<!---MARKER_GEN_END-->

View File

@@ -0,0 +1,15 @@
# docker compose bridge transformations create
<!---MARKER_GEN_START-->
Create a new transformation
### Options
| Name | Type | Default | Description |
|:---------------|:---------|:--------|:----------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `-f`, `--from` | `string` | | Existing transformation to copy (default: docker/compose-bridge-kubernetes) |
<!---MARKER_GEN_END-->

View File

@@ -0,0 +1,20 @@
# docker compose bridge transformations list
<!---MARKER_GEN_START-->
List available transformations
### Aliases
`docker compose bridge transformations list`, `docker compose bridge transformations ls`
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | `table` | Format the output. Values: [table \| json] |
| `-q`, `--quiet` | `bool` | | Only display transformer names |
<!---MARKER_GEN_END-->

View File

@@ -17,6 +17,7 @@ run `docker compose build` to rebuild it.
|:----------------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------|
| `--build-arg` | `stringArray` | | Set build-time variables for services |
| `--builder` | `string` | | Set builder to use |
| `--check` | `bool` | | Check build configuration |
| `--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 |

View File

@@ -5,19 +5,16 @@
It merges the Compose files set by `-f` flags, resolves variables in the Compose file, and expands short-notation into
the canonical format.
### Aliases
`docker compose config`, `docker compose convert`
### Options
| Name | Type | Default | Description |
|:--------------------------|:---------|:--------|:----------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--environment` | `bool` | | Print environment used for interpolation. |
| `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] |
| `--format` | `string` | | Format the output. Values: [yaml \| json] |
| `--hash` | `string` | | Print the service config hash, one per line. |
| `--images` | `bool` | | Print the image names, one per line. |
| `--lock-image-digests` | `bool` | | Produces an override file with image digests |
| `--no-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output |
| `--no-env-resolution` | `bool` | | Don't resolve service env files |
| `--no-interpolate` | `bool` | | Don't interpolate environment variables |

View File

@@ -6,6 +6,7 @@ pname: docker
plink: docker.yaml
cname:
- docker compose attach
- docker compose bridge
- docker compose build
- docker compose commit
- docker compose config
@@ -40,6 +41,7 @@ cname:
- docker compose watch
clink:
- docker_compose_attach.yaml
- docker_compose_bridge.yaml
- docker_compose_build.yaml
- docker_compose_commit.yaml
- docker_compose_config.yaml
@@ -167,7 +169,6 @@ options:
swarm: false
- option: progress
value_type: string
default_value: auto
description: Set type of progress output (auto, tty, plain, json, quiet)
deprecated: false
hidden: false

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
command: docker compose bridge
short: Convert compose files into another model
long: Convert compose files into another model
pname: docker compose
plink: docker_compose.yaml
cname:
- docker compose bridge convert
- docker compose bridge transformations
clink:
- docker_compose_bridge_convert.yaml
- docker_compose_bridge_transformations.yaml
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

@@ -0,0 +1,59 @@
command: docker compose bridge convert
short: |
Convert compose files to Kubernetes manifests, Helm charts, or another model
long: |
Convert compose files to Kubernetes manifests, Helm charts, or another model
usage: docker compose bridge convert
pname: docker compose bridge
plink: docker_compose_bridge.yaml
options:
- option: output
shorthand: o
value_type: string
default_value: out
description: The output directory for the Kubernetes resources
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: templates
value_type: string
description: Directory containing transformation templates
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: transformation
shorthand: t
value_type: stringArray
default_value: '[]'
description: |
Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)
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

@@ -0,0 +1,29 @@
command: docker compose bridge transformations
short: Manage transformation images
long: Manage transformation images
pname: docker compose bridge
plink: docker_compose_bridge.yaml
cname:
- docker compose bridge transformations create
- docker compose bridge transformations list
clink:
- docker_compose_bridge_transformations_create.yaml
- docker_compose_bridge_transformations_list.yaml
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

@@ -0,0 +1,36 @@
command: docker compose bridge transformations create
short: Create a new transformation
long: Create a new transformation
usage: docker compose bridge transformations create [OPTION] PATH
pname: docker compose bridge transformations
plink: docker_compose_bridge_transformations.yaml
options:
- option: from
shorthand: f
value_type: string
description: |
Existing transformation to copy (default: docker/compose-bridge-kubernetes)
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

@@ -0,0 +1,47 @@
command: docker compose bridge transformations list
aliases: docker compose bridge transformations list, docker compose bridge transformations ls
short: List available transformations
long: List available transformations
usage: docker compose bridge transformations list
pname: docker compose bridge transformations
plink: docker_compose_bridge_transformations.yaml
options:
- option: format
value_type: string
default_value: table
description: 'Format the output. Values: [table | json]'
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only display transformer names
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

View File

@@ -33,6 +33,16 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: check
value_type: bool
default_value: "false"
description: Check build configuration
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: compress
value_type: bool
default_value: "true"
@@ -108,7 +118,6 @@ options:
swarm: false
- option: progress
value_type: string
default_value: auto
description: Set type of ui output (auto, tty, plain, json, quiet)
deprecated: false
hidden: true

View File

@@ -1,5 +1,4 @@
command: docker compose config
aliases: docker compose config, docker compose convert
short: Parse, resolve and render compose file in canonical format
long: |-
`docker compose config` renders the actual data model to be applied on the Docker Engine.
@@ -21,7 +20,6 @@ options:
swarm: false
- option: format
value_type: string
default_value: yaml
description: 'Format the output. Values: [yaml | json]'
deprecated: false
hidden: false
@@ -48,6 +46,16 @@ options:
experimentalcli: false
kubernetes: false
swarm: false
- option: lock-image-digests
value_type: bool
default_value: "false"
description: Produces an override file with image digests
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: no-consistency
value_type: bool
default_value: "false"

106
go.mod
View File

@@ -8,15 +8,16 @@ require (
github.com/Microsoft/go-winio v0.6.2
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/buger/goterm v1.0.4
github.com/compose-spec/compose-go/v2 v2.6.0
github.com/containerd/containerd/v2 v2.0.4
github.com/compose-spec/compose-go/v2 v2.6.5
github.com/containerd/containerd/v2 v2.1.2
github.com/containerd/errdefs v1.0.0
github.com/containerd/platforms v1.0.0-rc.1
github.com/davecgh/go-spew v1.1.1
github.com/distribution/reference v0.6.0
github.com/docker/buildx v0.23.0
github.com/docker/cli v28.1.0+incompatible
github.com/docker/cli-docs-tool v0.9.0
github.com/docker/docker v28.1.0+incompatible
github.com/docker/buildx v0.25.0
github.com/docker/cli v28.2.2+incompatible
github.com/docker/cli-docs-tool v0.10.0
github.com/docker/docker v28.2.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
@@ -28,7 +29,7 @@ require (
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.21.0
github.com/moby/buildkit v0.23.0
github.com/moby/go-archive v0.1.0
github.com/moby/patternmatcher v0.6.0
github.com/moby/sys/atomicwriter v0.1.0
@@ -37,27 +38,24 @@ require (
github.com/opencontainers/go-digest v1.0.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
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/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.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.34.0
go.opentelemetry.io/otel/sdk v1.34.0
go.opentelemetry.io/otel/trace v1.34.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
go.opentelemetry.io/otel/metric v1.35.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
go.uber.org/goleak v1.3.0
go.uber.org/mock v0.5.1
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/sync v0.13.0
golang.org/x/sys v0.32.0
google.golang.org/grpc v1.71.1
go.uber.org/mock v0.5.2
golang.org/x/sync v0.15.0
golang.org/x/sys v0.33.0
google.golang.org/grpc v1.73.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v1.0.1
@@ -65,7 +63,6 @@ require (
require (
dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
@@ -85,10 +82,9 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/containerd/api v1.8.0 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/containerd/containerd/api v1.9.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
@@ -104,9 +100,9 @@ require (
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
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
@@ -118,11 +114,10 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/in-toto/in-toto-golang v0.5.0 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
github.com/josharian/intern v1.0.0 // indirect
@@ -133,13 +128,13 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.4.0 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
@@ -156,18 +151,20 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250410151801-5b74a7ad7583 // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/x448/float16 v0.8.4 // indirect
@@ -175,36 +172,35 @@ require (
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
github.com/zclconf/go-cty v1.16.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // 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
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.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
k8s.io/client-go v0.31.2 // indirect
k8s.io/api v0.32.3 // indirect
k8s.io/apimachinery v0.32.3 // indirect
k8s.io/client-go v0.32.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

240
go.sum
View File

@@ -2,8 +2,6 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 h1:dIScnXFlF784X79oi7MzVT6GWqr/W1uUt0pB5CsDs9M=
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
@@ -16,8 +14,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
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=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg=
github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y=
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ=
@@ -82,16 +80,16 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.6.0 h1:/+oBD2ixSENOeN/TlJqWZmUak0xM8A7J08w/z661Wd4=
github.com/compose-spec/compose-go/v2 v2.6.0/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
github.com/compose-spec/compose-go/v2 v2.6.5 h1:H7xP5OMKdkN2p0brx01slxIU6dE/q6ybbG+jozPtIqk=
github.com/compose-spec/compose-go/v2 v2.6.5/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU=
github.com/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.4 h1:+r7yJMwhTfMm3CDyiBjMBQO8a9CTBxL2Bg/JtqtIwB8=
github.com/containerd/containerd/v2 v2.0.4/go.mod h1:5j9QUUaV/cy9ZeAx4S+8n9ffpf+iYnEj4jiExgcbuLY=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0=
github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI=
github.com/containerd/containerd/v2 v2.1.2 h1:4ZQxB+FVYmwXZgpBcKfar6ieppm3KC5C6FRKvtJ6DRU=
github.com/containerd/containerd/v2 v2.1.2/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
github.com/containerd/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=
@@ -102,8 +100,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nydus-snapshotter v0.15.0 h1:RqZRs1GPeM6T3wmuxJV9u+2Rg4YETVMwTmiDeX+iWC8=
github.com/containerd/nydus-snapshotter v0.15.0/go.mod h1:biq0ijpeZe0I5yZFSJyHzFSjjRZQ7P7y/OuHyd7hYOw=
github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg=
github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM=
github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E=
github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
@@ -127,17 +125,19 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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.23.0 h1:qoYhuWyZ6PVCrWbkxClLzBWDBCUkyFK6Chjzg6nU+V8=
github.com/docker/buildx v0.23.0/go.mod h1:y/6Zf/y3Bf0zTWqgg8PuNFATcqnuhFmQuNf4VyrnPtg=
github.com/docker/cli v28.1.0+incompatible h1:WiJhUBbuIH/BsJtth+C1hPwra4P0nsKJiWy9ie5My5s=
github.com/docker/cli v28.1.0+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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/buildx v0.25.0 h1:qs5WxBo0wQKSXcQ+v6UhWaeM2Pu+95ZCymaimRzInaE=
github.com/docker/buildx v0.25.0/go.mod h1:xJcOeBhz49tgqN174MMGuOU4bxNmgfaLnZn7Gm641EE=
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.1.0+incompatible h1:4iqpcWQCt3Txcz7iWIb1U3SZ/n9ffo4U+ryY5/3eOp0=
github.com/docker/docker v28.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@@ -175,13 +175,14 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -217,8 +218,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
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=
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -228,8 +229,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -244,10 +245,8 @@ github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/in-toto/in-toto-golang v0.5.0 h1:hb8bgwr0M2hGdDsLjkJ3ZqJ8JFLL/tgYdAxF/XEFBbY=
github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1GdHMCq8+WPxw8/BE=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
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=
@@ -300,8 +299,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.16/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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@@ -318,8 +317,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.21.0 h1:+z4vVqgt0spLrOSxi4DLedRbIh2gbNVlZ5q4rsnNp60=
github.com/moby/buildkit v0.21.0/go.mod h1:mBq0D44uCyz2PdX8T/qym5LBbkBO3GGv0wqgX9ABYYw=
github.com/moby/buildkit v0.23.0 h1:HV+u7xM2IZhAjVautFR2l5FNhkxFR0jhF5ILXyc3398=
github.com/moby/buildkit v0.23.0/go.mod h1:v5jMDvQgUyidk3wu3NvVAAd5JJo83nfet9Gf/o0+EAQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
@@ -328,8 +327,8 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
@@ -366,22 +365,22 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.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/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8=
github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
@@ -402,8 +401,8 @@ github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -412,24 +411,24 @@ github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQy
github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc h1:zAsgcP8MhzAbhMnB1QQ2O7ZhWYVGYSR2iVcjzQuPV+o=
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc/go.mod h1:S8xSOnV3CgpNrWd0GQ/OoQfMtlg2uPRSuTzcSGrzwK8=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE=
github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
@@ -441,8 +440,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk=
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@@ -477,16 +476,16 @@ github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
github.com/tonistiigi/fsutil v0.0.0-20250410151801-5b74a7ad7583 h1:mK+ZskNt7SG4dxfKIi27C7qHAQzyjAVt1iyTf0hmsNc=
github.com/tonistiigi/fsutil v0.0.0-20250410151801-5b74a7ad7583/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -501,44 +500,44 @@ github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtX
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=
github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w=
github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70=
github.com/zclconf/go-cty v1.16.2/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/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko=
go.opentelemetry.io/contrib/instrumentation/net/http/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.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU=
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.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.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -549,8 +548,6 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -561,15 +558,14 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/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=
@@ -577,8 +573,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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.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=
@@ -600,8 +596,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.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=
@@ -626,25 +622,25 @@ 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-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/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/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=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@@ -665,22 +661,22 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
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 v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc=

View File

@@ -17,10 +17,8 @@
package desktop
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
@@ -29,7 +27,6 @@ import (
"github.com/docker/compose/v2/internal"
"github.com/docker/compose/v2/internal/memnet"
"github.com/r3labs/sse"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
@@ -130,212 +127,6 @@ func (c *Client) FeatureFlags(ctx context.Context) (FeatureFlagResponse, error)
return ret, nil
}
type GetFileSharesConfigResponse struct {
Active bool `json:"active"`
Compose struct {
ManageBindMounts bool `json:"manageBindMounts"`
}
}
func (c *Client) GetFileSharesConfig(ctx context.Context) (*GetFileSharesConfigResponse, error) {
req, err := c.newRequest(ctx, http.MethodGet, "/mutagen/file-shares/config", http.NoBody)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return nil, newHTTPStatusCodeError(resp)
}
var ret GetFileSharesConfigResponse
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
return nil, err
}
return &ret, nil
}
type CreateFileShareRequest struct {
HostPath string `json:"hostPath"`
Labels map[string]string `json:"labels,omitempty"`
}
type CreateFileShareResponse struct {
FileShareID string `json:"fileShareID"`
}
func (c *Client) CreateFileShare(ctx context.Context, r CreateFileShareRequest) (*CreateFileShareResponse, error) {
rawBody, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
req, err := c.newRequest(ctx, http.MethodPost, "/mutagen/file-shares", bytes.NewReader(rawBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
errBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(errBody))
}
var ret CreateFileShareResponse
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
return nil, err
}
return &ret, nil
}
type FileShareReceiverState struct {
TotalReceivedSize uint64 `json:"totalReceivedSize"`
}
type FileShareEndpoint struct {
Path string `json:"path"`
TotalFileSize uint64 `json:"totalFileSize,omitempty"`
StagingProgress *FileShareReceiverState `json:"stagingProgress"`
}
type FileShareSession struct {
SessionID string `json:"identifier"`
Alpha FileShareEndpoint `json:"alpha"`
Beta FileShareEndpoint `json:"beta"`
Labels map[string]string `json:"labels"`
Status string `json:"status"`
}
func (c *Client) ListFileShares(ctx context.Context) ([]FileShareSession, error) {
req, err := c.newRequest(ctx, http.MethodGet, "/mutagen/file-shares", http.NoBody)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return nil, newHTTPStatusCodeError(resp)
}
var ret []FileShareSession
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
return nil, err
}
return ret, nil
}
func (c *Client) DeleteFileShare(ctx context.Context, id string) error {
req, err := c.newRequest(ctx, http.MethodDelete, "/mutagen/file-shares/"+id, http.NoBody)
if err != nil {
return err
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return newHTTPStatusCodeError(resp)
}
return nil
}
type EventMessage[T any] struct {
Value T
Error error
}
func newHTTPStatusCodeError(resp *http.Response) error {
r := io.LimitReader(resp.Body, 2048)
body, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("http status code %d", resp.StatusCode)
}
return fmt.Errorf("http status code %d: %s", resp.StatusCode, string(body))
}
func (c *Client) StreamFileShares(ctx context.Context) (<-chan EventMessage[[]FileShareSession], error) {
req, err := c.newRequest(ctx, http.MethodGet, "/mutagen/file-shares/stream", http.NoBody)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
_ = resp.Body.Close()
return nil, newHTTPStatusCodeError(resp)
}
events := make(chan EventMessage[[]FileShareSession])
go func(ctx context.Context) {
defer func() {
_ = resp.Body.Close()
close(events)
}()
if err := readEvents(ctx, resp.Body, events); err != nil {
select {
case <-ctx.Done():
case events <- EventMessage[[]FileShareSession]{Error: err}:
}
}
}(ctx)
return events, nil
}
func readEvents[T any](ctx context.Context, r io.Reader, events chan<- EventMessage[T]) error {
eventReader := sse.NewEventStreamReader(r)
for {
msg, err := eventReader.ReadEvent()
if errors.Is(err, io.EOF) {
return nil
} else if err != nil {
return fmt.Errorf("reading events: %w", err)
}
msg = bytes.TrimPrefix(msg, []byte("data: "))
var event T
if err := json.Unmarshal(msg, &event); err != nil {
return err
}
select {
case <-ctx.Done():
return context.Cause(ctx)
case events <- EventMessage[T]{Value: event}:
// event was sent to channel, read next
}
}
}
func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, backendURL(path), body)
if err != nil {

View File

@@ -1,384 +0,0 @@
/*
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 desktop
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/docker/compose/v2/internal/paths"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/go-units"
"github.com/sirupsen/logrus"
)
// fileShareProgressID is the identifier used for the root grouping of file
// share events in the progress writer.
const fileShareProgressID = "Synchronized File Shares"
// RemoveFileSharesForProject removes any Synchronized File Shares that were
// created by Compose for this project in the past if possible.
//
// Errors are not propagated; they are only sent to the progress writer.
func RemoveFileSharesForProject(ctx context.Context, c *Client, projectName string) {
w := progress.ContextWriter(ctx)
existing, err := c.ListFileShares(ctx)
if err != nil {
w.TailMsgf("Synchronized File Shares not removed due to error: %v", err)
return
}
// filter the list first, so we can early return and not show the event if
// there's no sessions to clean up
var toRemove []FileShareSession
for _, share := range existing {
if share.Labels["com.docker.compose.project"] == projectName {
toRemove = append(toRemove, share)
}
}
if len(toRemove) == 0 {
return
}
w.Event(progress.NewEvent(fileShareProgressID, progress.Working, "Removing"))
rootResult := progress.Done
defer func() {
w.Event(progress.NewEvent(fileShareProgressID, rootResult, ""))
}()
for _, share := range toRemove {
shareID := share.Labels["com.docker.desktop.mutagen.file-share.id"]
if shareID == "" {
w.Event(progress.Event{
ID: share.Alpha.Path,
ParentID: fileShareProgressID,
Status: progress.Warning,
StatusText: "Invalid",
})
continue
}
w.Event(progress.Event{
ID: share.Alpha.Path,
ParentID: fileShareProgressID,
Status: progress.Working,
})
var status progress.EventStatus
var statusText string
if err := c.DeleteFileShare(ctx, shareID); err != nil {
// TODO(milas): Docker Desktop is doing weird things with error responses,
// once fixed, we can return proper error types from the client
if strings.Contains(err.Error(), "file share in use") {
status = progress.Warning
statusText = "Resource is still in use"
if rootResult != progress.Error {
// error takes precedence over warning
rootResult = progress.Warning
}
} else {
logrus.Debugf("Error deleting file share %q: %v", shareID, err)
status = progress.Error
rootResult = progress.Error
}
} else {
logrus.Debugf("Deleted file share: %s", shareID)
status = progress.Done
}
w.Event(progress.Event{
ID: share.Alpha.Path,
ParentID: fileShareProgressID,
Status: status,
StatusText: statusText,
})
}
}
// FileShareManager maps between Compose bind mounts and Desktop File Shares
// state.
type FileShareManager struct {
mu sync.Mutex
cli *Client
projectName string
hostPaths []string
// state holds session status keyed by file share ID.
state map[string]*FileShareSession
}
func NewFileShareManager(cli *Client, projectName string, hostPaths []string) *FileShareManager {
return &FileShareManager{
cli: cli,
projectName: projectName,
hostPaths: hostPaths,
state: make(map[string]*FileShareSession),
}
}
// EnsureExists looks for existing File Shares or creates new ones for the
// host paths.
//
// This function blocks until each share reaches steady state, at which point
// flow can continue.
func (m *FileShareManager) EnsureExists(ctx context.Context) (err error) {
w := progress.ContextWriter(ctx)
w.Event(progress.NewEvent(fileShareProgressID, progress.Working, ""))
defer func() {
if err != nil {
w.Event(progress.NewEvent(fileShareProgressID, progress.Error, ""))
} else {
w.Event(progress.NewEvent(fileShareProgressID, progress.Done, ""))
}
}()
wait := &waiter{
shareIDs: make(map[string]struct{}),
done: make(chan struct{}),
}
handler := m.eventHandler(w, wait)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// stream session events to update internal state for project
monitorErr := make(chan error, 1)
go func() {
defer close(monitorErr)
if err := m.watch(ctx, handler); err != nil && ctx.Err() == nil {
monitorErr <- err
}
}()
if err := m.initialize(ctx, wait, handler); err != nil {
return err
}
waitCh := wait.start()
if waitCh != nil {
select {
case <-ctx.Done():
return context.Cause(ctx)
case err := <-monitorErr:
if err != nil {
return fmt.Errorf("watching file share sessions: %w", err)
} else if ctx.Err() == nil {
// this indicates a bug - it should not stop w/o an error if the context is still active
return errors.New("file share session watch stopped unexpectedly")
}
case <-wait.start():
// everything is done
}
}
return nil
}
// initialize finds existing shares or creates new ones for the host paths.
//
// Once a share is found/created, its progress is monitored via the watch.
func (m *FileShareManager) initialize(ctx context.Context, wait *waiter, handler func(FileShareSession)) error {
// the watch is already running in the background, so the lock is taken
// throughout to prevent interleaving writes
m.mu.Lock()
defer m.mu.Unlock()
existing, err := m.cli.ListFileShares(ctx)
if err != nil {
return err
}
for _, path := range m.hostPaths {
var fileShareID string
var fss *FileShareSession
if fss = findExistingShare(path, existing); fss != nil {
fileShareID = fss.Beta.Path
logrus.Debugf("Found existing suitable file share %s for path %q [%s]", fileShareID, path, fss.Alpha.Path)
wait.addShare(fileShareID)
handler(*fss)
continue
} else {
req := CreateFileShareRequest{
HostPath: path,
Labels: map[string]string{
"com.docker.compose.project": m.projectName,
},
}
createResp, err := m.cli.CreateFileShare(ctx, req)
if err != nil {
return fmt.Errorf("creating file share: %w", err)
}
fileShareID = createResp.FileShareID
fss = m.state[fileShareID]
logrus.Debugf("Created file share %s for path %q", fileShareID, path)
}
wait.addShare(fileShareID)
if fss != nil {
handler(*fss)
}
}
return nil
}
func (m *FileShareManager) watch(ctx context.Context, handler func(FileShareSession)) error {
events, err := m.cli.StreamFileShares(ctx)
if err != nil {
return fmt.Errorf("streaming file shares: %w", err)
}
for {
select {
case <-ctx.Done():
return nil
case event := <-events:
if event.Error != nil {
return fmt.Errorf("reading file share events: %w", event.Error)
}
// closure for lock
func() {
m.mu.Lock()
defer m.mu.Unlock()
for _, fss := range event.Value {
handler(fss)
}
}()
}
}
}
// eventHandler updates internal state, keeps track of in-progress syncs, and
// prints relevant events to progress.
func (m *FileShareManager) eventHandler(w progress.Writer, wait *waiter) func(fss FileShareSession) {
return func(fss FileShareSession) {
fileShareID := fss.Beta.Path
shouldPrint := wait.isWatching(fileShareID)
forProject := fss.Labels[api.ProjectLabel] == m.projectName
if shouldPrint || forProject {
m.state[fileShareID] = &fss
}
var percent int
var current, total int64
if fss.Beta.StagingProgress != nil {
current = int64(fss.Beta.StagingProgress.TotalReceivedSize)
} else {
current = int64(fss.Beta.TotalFileSize)
}
total = int64(fss.Alpha.TotalFileSize)
if total != 0 {
percent = int(current * 100 / total)
}
var status progress.EventStatus
var text string
switch {
case strings.HasPrefix(fss.Status, "halted"):
wait.shareDone(fileShareID)
status = progress.Error
case fss.Status == "watching":
wait.shareDone(fileShareID)
status = progress.Done
percent = 100
case fss.Status == "staging-beta":
status = progress.Working
// TODO(milas): the printer doesn't style statuses for children nicely
text = fmt.Sprintf(" Syncing (%7s / %-7s)",
units.HumanSize(float64(current)),
units.HumanSize(float64(total)),
)
default:
// catch-all for various other transitional statuses
status = progress.Working
}
evt := progress.Event{
ID: fss.Alpha.Path,
Status: status,
Text: text,
ParentID: fileShareProgressID,
Current: current,
Total: total,
Percent: percent,
}
if shouldPrint {
w.Event(evt)
}
}
}
func findExistingShare(path string, existing []FileShareSession) *FileShareSession {
for _, share := range existing {
if paths.IsChild(share.Alpha.Path, path) {
return &share
}
}
return nil
}
type waiter struct {
mu sync.Mutex
shareIDs map[string]struct{}
done chan struct{}
}
func (w *waiter) addShare(fileShareID string) {
w.mu.Lock()
defer w.mu.Unlock()
w.shareIDs[fileShareID] = struct{}{}
}
func (w *waiter) isWatching(fileShareID string) bool {
w.mu.Lock()
defer w.mu.Unlock()
_, ok := w.shareIDs[fileShareID]
return ok
}
// start returns a channel to wait for any outstanding shares to be ready.
//
// If no shares are registered when this is called, nil is returned.
func (w *waiter) start() <-chan struct{} {
w.mu.Lock()
defer w.mu.Unlock()
if len(w.shareIDs) == 0 {
return nil
}
if w.done == nil {
w.done = make(chan struct{})
}
return w.done
}
func (w *waiter) shareDone(fileShareID string) {
w.mu.Lock()
defer w.mu.Unlock()
delete(w.shareIDs, fileShareID)
if len(w.shareIDs) == 0 && w.done != nil {
close(w.done)
w.done = nil
}
}

View File

@@ -23,6 +23,7 @@ import (
"fmt"
"net/http"
"path/filepath"
"slices"
"time"
pusherrors "github.com/containerd/containerd/v2/core/remotes/errors"
@@ -104,7 +105,9 @@ func PushManifest(
) error {
// Check if we need an extra empty layer for the manifest config
if ociVersion == api.OCIVersion1_1 || ociVersion == "" {
layers = append(layers, Pushable{Descriptor: v1.DescriptorEmptyJSON, Data: []byte("{}")})
if err := resolver.Push(ctx, named, v1.DescriptorEmptyJSON, v1.DescriptorEmptyJSON.Data); err != nil {
return err
}
}
// prepare to push the manifest by pushing the layers
layerDescriptors := make([]v1.Descriptor, len(layers))
@@ -157,14 +160,7 @@ func isNonAuthClientError(statusCode int) bool {
// not a client error
return false
}
for _, v := range clientAuthStatusCodes {
if statusCode == v {
// client auth error
return false
}
}
// any other 4xx client error
return true
return !slices.Contains(clientAuthStatusCodes, statusCode)
}
func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pushable, error) {

View File

@@ -1,2 +0,0 @@
The Docker End User License Agreement (https://www.docker.com/legal/docker-software-end-user-license-agreement) describes Docker's Terms for this software.
By downloading, accessing, or using this software you expressly accept and agree to the Terms set out in the Docker End User License Agreement.

View File

@@ -19,12 +19,13 @@ package api
import (
"context"
"fmt"
"slices"
"strings"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/docker/cli/opts"
"github.com/docker/compose/v2/pkg/utils"
)
// Service manages a compose project
@@ -78,13 +79,13 @@ type Service interface {
// Publish executes the equivalent to a `compose publish`
Publish(ctx context.Context, project *types.Project, repository string, options PublishOptions) error
// Images executes the equivalent of a `compose images`
Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
Images(ctx context.Context, projectName string, options ImagesOptions) (map[string]ImageSummary, error)
// MaxConcurrency defines upper limit for concurrent operations against engine API
MaxConcurrency(parallel int)
// DryRunMode defines if dry run applies to the command
DryRunMode(ctx context.Context, dryRun bool) (context.Context, error)
// Watch services' development context and sync/notify/rebuild/restart on changes
Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
Watch(ctx context.Context, project *types.Project, options WatchOptions) error
// Viz generates a graphviz graph of the project services
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
// Wait blocks until at least one of the services' container exits
@@ -126,9 +127,10 @@ const WatchLogger = "#watch"
// WatchOptions group options of the Watch API
type WatchOptions struct {
Build *BuildOptions
LogTo LogConsumer
Prune bool
Build *BuildOptions
LogTo LogConsumer
Prune bool
Services []string
}
// BuildOptions group options of the Build API
@@ -157,13 +159,17 @@ type BuildOptions struct {
Builder string
// Print don't actually run builder but print equivalent build config
Print bool
// Check let builder validate build configuration
Check bool
// Provenance
Provenance bool
}
// Apply mutates project according to build options
func (o BuildOptions) Apply(project *types.Project) error {
platform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
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)
}
@@ -171,13 +177,13 @@ func (o BuildOptions) Apply(project *types.Project) error {
continue
}
if platform != "" {
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, platform) {
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, platform) {
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, platform)
}
service.Platform = platform
}
if service.Platform != "" {
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, service.Platform) {
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
}
}
@@ -345,7 +351,7 @@ type RemoveOptions struct {
// RunOptions group options of the Run API
type RunOptions struct {
Build *BuildOptions
CreateOptions
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
Project *types.Project
Name string
@@ -365,8 +371,6 @@ type RunOptions struct {
Privileged bool
UseNetworkAliases bool
NoDeps bool
// QuietPull makes the pulling process quiet
QuietPull bool
// used by exec
Index int
}
@@ -531,12 +535,12 @@ type ContainerProcSummary struct {
// ImageSummary holds container image description
type ImageSummary struct {
ID string
ContainerName string
Repository string
Tag string
Size int64
LastTagTime time.Time
ID string
Repository string
Tag string
Platform platforms.Platform
Size int64
LastTagTime time.Time
}
// ServiceStatus hold status about a service

View File

@@ -34,6 +34,7 @@ import (
"github.com/docker/buildx/util/imagetools"
"github.com/docker/cli/cli/command"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/checkpoint"
containerType "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
@@ -205,18 +206,18 @@ func (d *DryRunClient) CopyToContainer(ctx context.Context, container, path stri
return nil
}
func (d *DryRunClient) ImageBuild(ctx context.Context, reader io.Reader, options moby.ImageBuildOptions) (moby.ImageBuildResponse, error) {
func (d *DryRunClient) ImageBuild(ctx context.Context, reader io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) {
jsonMessage, err := json.Marshal(&jsonmessage.JSONMessage{
Status: fmt.Sprintf("%[1]sSuccessfully built: dryRunID\n%[1]sSuccessfully tagged: %[2]s\n", DRYRUN_PREFIX, options.Tags[0]),
Progress: &jsonmessage.JSONProgress{},
ID: "",
})
if err != nil {
return moby.ImageBuildResponse{}, err
return build.ImageBuildResponse{}, err
}
rc := io.NopCloser(bytes.NewReader(jsonMessage))
return moby.ImageBuildResponse{
return build.ImageBuildResponse{
Body: rc,
OSType: "",
}, nil
@@ -334,11 +335,11 @@ func (d *DryRunClient) ContainerExecStart(ctx context.Context, execID string, co
// Functions delegated to original APIClient (not used by Compose or not modifying the Compose stack
func (d *DryRunClient) ConfigList(ctx context.Context, options moby.ConfigListOptions) ([]swarm.Config, error) {
func (d *DryRunClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
return d.apiClient.ConfigList(ctx, options)
}
func (d *DryRunClient) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (moby.ConfigCreateResponse, error) {
func (d *DryRunClient) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
return d.apiClient.ConfigCreate(ctx, config)
}
@@ -422,7 +423,7 @@ func (d *DryRunClient) DistributionInspect(ctx context.Context, imageName, encod
return d.apiClient.DistributionInspect(ctx, imageName, encodedRegistryAuth)
}
func (d *DryRunClient) BuildCachePrune(ctx context.Context, opts moby.BuildCachePruneOptions) (*moby.BuildCachePruneReport, error) {
func (d *DryRunClient) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
return d.apiClient.BuildCachePrune(ctx, opts)
}
@@ -470,11 +471,11 @@ func (d *DryRunClient) NodeInspectWithRaw(ctx context.Context, nodeID string) (s
return d.apiClient.NodeInspectWithRaw(ctx, nodeID)
}
func (d *DryRunClient) NodeList(ctx context.Context, options moby.NodeListOptions) ([]swarm.Node, error) {
func (d *DryRunClient) NodeList(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) {
return d.apiClient.NodeList(ctx, options)
}
func (d *DryRunClient) NodeRemove(ctx context.Context, nodeID string, options moby.NodeRemoveOptions) error {
func (d *DryRunClient) NodeRemove(ctx context.Context, nodeID string, options swarm.NodeRemoveOptions) error {
return d.apiClient.NodeRemove(ctx, nodeID, options)
}
@@ -538,15 +539,15 @@ func (d *DryRunClient) PluginCreate(ctx context.Context, createContext io.Reader
return d.apiClient.PluginCreate(ctx, createContext, options)
}
func (d *DryRunClient) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options moby.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {
func (d *DryRunClient) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options swarm.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {
return d.apiClient.ServiceCreate(ctx, service, options)
}
func (d *DryRunClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options moby.ServiceInspectOptions) (swarm.Service, []byte, error) {
func (d *DryRunClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) {
return d.apiClient.ServiceInspectWithRaw(ctx, serviceID, options)
}
func (d *DryRunClient) ServiceList(ctx context.Context, options moby.ServiceListOptions) ([]swarm.Service, error) {
func (d *DryRunClient) ServiceList(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) {
return d.apiClient.ServiceList(ctx, options)
}
@@ -554,7 +555,7 @@ func (d *DryRunClient) ServiceRemove(ctx context.Context, serviceID string) erro
return d.apiClient.ServiceRemove(ctx, serviceID)
}
func (d *DryRunClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options moby.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
func (d *DryRunClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
return d.apiClient.ServiceUpdate(ctx, serviceID, version, service, options)
}
@@ -570,7 +571,7 @@ func (d *DryRunClient) TaskInspectWithRaw(ctx context.Context, taskID string) (s
return d.apiClient.TaskInspectWithRaw(ctx, taskID)
}
func (d *DryRunClient) TaskList(ctx context.Context, options moby.TaskListOptions) ([]swarm.Task, error) {
func (d *DryRunClient) TaskList(ctx context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) {
return d.apiClient.TaskList(ctx, options)
}
@@ -582,7 +583,7 @@ func (d *DryRunClient) SwarmJoin(ctx context.Context, req swarm.JoinRequest) err
return d.apiClient.SwarmJoin(ctx, req)
}
func (d *DryRunClient) SwarmGetUnlockKey(ctx context.Context) (moby.SwarmUnlockKeyResponse, error) {
func (d *DryRunClient) SwarmGetUnlockKey(ctx context.Context) (swarm.UnlockKeyResponse, error) {
return d.apiClient.SwarmGetUnlockKey(ctx)
}
@@ -602,11 +603,11 @@ func (d *DryRunClient) SwarmUpdate(ctx context.Context, version swarm.Version, s
return d.apiClient.SwarmUpdate(ctx, version, swarmSpec, flags)
}
func (d *DryRunClient) SecretList(ctx context.Context, options moby.SecretListOptions) ([]swarm.Secret, error) {
func (d *DryRunClient) SecretList(ctx context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) {
return d.apiClient.SecretList(ctx, options)
}
func (d *DryRunClient) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (moby.SecretCreateResponse, error) {
func (d *DryRunClient) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error) {
return d.apiClient.SecretCreate(ctx, secret)
}

218
pkg/bridge/convert.go Normal file
View File

@@ -0,0 +1,218 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package bridge
import (
"context"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"strconv"
"github.com/compose-spec/compose-go/v2/types"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/cli/cli/command"
cli "github.com/docker/cli/cli/command/container"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/go-connections/nat"
"gopkg.in/yaml.v3"
)
type ConvertOptions struct {
Output string
Templates string
Transformations []string
}
func Convert(ctx context.Context, dockerCli command.Cli, project *types.Project, opts ConvertOptions) error {
if len(opts.Transformations) == 0 {
opts.Transformations = []string{DefaultTransformerImage}
}
// Load image references, secrets and configs, also expose ports
project, err := LoadAdditionalResources(ctx, dockerCli, project)
if err != nil {
return err
}
// for user to rely on compose.yaml attribute names, not go struct ones, we marshall back into YAML
raw, err := project.MarshalYAML(types.WithSecretContent)
// Marshall to YAML
if err != nil {
return fmt.Errorf("cannot render project into yaml: %w", err)
}
var model map[string]any
err = yaml.Unmarshal(raw, &model)
if err != nil {
return fmt.Errorf("cannot render project into yaml: %w", err)
}
if opts.Output != "" {
_ = os.RemoveAll(opts.Output)
err := os.MkdirAll(opts.Output, 0o744)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("cannot create output folder: %w", err)
}
}
// Run Transformers images
return convert(ctx, dockerCli, model, opts)
}
func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, opts ConvertOptions) error {
raw, err := yaml.Marshal(model)
if err != nil {
return err
}
dir := os.TempDir()
composeYaml := filepath.Join(dir, "compose.yaml")
err = os.WriteFile(composeYaml, raw, 0o600)
if err != nil {
return err
}
out, err := filepath.Abs(opts.Output)
if err != nil {
return err
}
binds := []string{
fmt.Sprintf("%s:%s", dir, "/in"),
fmt.Sprintf("%s:%s", out, "/out"),
}
if opts.Templates != "" {
templateDir, err := filepath.Abs(opts.Templates)
if err != nil {
return err
}
binds = append(binds, fmt.Sprintf("%s:%s", templateDir, "/templates"))
}
for _, transformation := range opts.Transformations {
_, err = inspectWithPull(ctx, dockerCli, transformation)
if err != nil {
return err
}
usr, err := user.Current()
if err != nil {
return err
}
created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
Image: transformation,
Env: []string{"LICENSE_AGREEMENT=true"},
User: usr.Uid,
}, &container.HostConfig{
AutoRemove: true,
Binds: binds,
}, &network.NetworkingConfig{}, nil, "")
if err != nil {
return err
}
err = cli.RunStart(ctx, dockerCli, &cli.StartOptions{
Attach: true,
Containers: []string{created.ID},
})
if err != nil {
return err
}
}
return nil
}
// LoadAdditionalResources loads additional resources from the project, such as image references, secrets, configs and exposed ports
func LoadAdditionalResources(ctx context.Context, dockerCLI command.Cli, project *types.Project) (*types.Project, error) {
for name, service := range project.Services {
imageName := api.GetImageNameOrDefault(service, project.Name)
inspect, err := inspectWithPull(ctx, dockerCLI, imageName)
if err != nil {
return nil, err
}
service.Image = imageName
exposed := utils.Set[string]{}
exposed.AddAll(service.Expose...)
for port := range inspect.Config.ExposedPorts {
exposed.Add(nat.Port(port).Port())
}
for _, port := range service.Ports {
exposed.Add(strconv.Itoa(int(port.Target)))
}
service.Expose = exposed.Elements()
project.Services[name] = service
}
for name, secret := range project.Secrets {
f, err := loadFileObject(types.FileObjectConfig(secret))
if err != nil {
return nil, err
}
project.Secrets[name] = types.SecretConfig(f)
}
for name, config := range project.Configs {
f, err := loadFileObject(types.FileObjectConfig(config))
if err != nil {
return nil, err
}
project.Configs[name] = types.ConfigObjConfig(f)
}
return project, nil
}
func loadFileObject(conf types.FileObjectConfig) (types.FileObjectConfig, error) {
if !conf.External {
switch {
case conf.Environment != "":
conf.Content = os.Getenv(conf.Environment)
case conf.File != "":
bytes, err := os.ReadFile(conf.File)
if err != nil {
return conf, err
}
conf.Content = string(bytes)
}
}
return conf, nil
}
func inspectWithPull(ctx context.Context, dockerCli command.Cli, imageName string) (image.InspectResponse, error) {
inspect, err := dockerCli.Client().ImageInspect(ctx, imageName)
if cerrdefs.IsNotFound(err) {
var stream io.ReadCloser
stream, err = dockerCli.Client().ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {
return image.InspectResponse{}, err
}
defer func() { _ = stream.Close() }()
err = jsonmessage.DisplayJSONMessagesToStream(stream, dockerCli.Out(), nil)
if err != nil {
return image.InspectResponse{}, err
}
if inspect, err = dockerCli.Client().ImageInspect(ctx, imageName); err != nil {
return image.InspectResponse{}, err
}
}
return inspect, err
}

118
pkg/bridge/transformers.go Normal file
View File

@@ -0,0 +1,118 @@
/*
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 bridge
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/moby/go-archive"
)
const (
TransformerLabel = "com.docker.compose.bridge"
DefaultTransformerImage = "docker/compose-bridge-kubernetes"
)
type CreateTransformerOptions struct {
Dest string
From string
}
func CreateTransformer(ctx context.Context, dockerCli command.Cli, options CreateTransformerOptions) error {
if options.From == "" {
options.From = DefaultTransformerImage
}
out, err := filepath.Abs(options.Dest)
if err != nil {
return err
}
if _, err := os.Stat(out); err == nil {
return fmt.Errorf("output folder %s already exists", out)
}
tmpl := filepath.Join(out, "templates")
err = os.MkdirAll(tmpl, 0o744)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("cannot create output folder: %w", err)
}
if err := command.ValidateOutputPath(out); err != nil {
return err
}
created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
Image: options.From,
}, &container.HostConfig{}, &network.NetworkingConfig{}, nil, "")
defer func() {
_ = dockerCli.Client().ContainerRemove(context.Background(), created.ID, container.RemoveOptions{Force: true})
}()
if err != nil {
return err
}
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, "/templates")
if err != nil {
return err
}
defer func() {
_ = content.Close()
}()
srcInfo := archive.CopyInfo{
Path: "/templates",
Exists: true,
IsDir: stat.Mode.IsDir(),
}
preArchive := content
if srcInfo.RebaseName != "" {
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
}
if err := archive.CopyTo(preArchive, srcInfo, out); err != nil {
return err
}
dockerfile := `FROM docker/compose-bridge-transformer
LABEL com.docker.compose.bridge=transformation
COPY templates /templates
`
if err := os.WriteFile(filepath.Join(out, "Dockerfile"), []byte(dockerfile), 0o700); err != nil {
return err
}
_, err = fmt.Fprintf(dockerCli.Out(), "Transformer created in %q\n", out)
return err
}
func ListTransformers(ctx context.Context, dockerCli command.Cli) ([]image.Summary, error) {
api := dockerCli.Client()
return api.ImageList(ctx, image.ListOptions{
Filters: filters.NewArgs(
filters.Arg("label", fmt.Sprintf("%s=%s", TransformerLabel, "transformation")),
),
})
}

90
pkg/compose/apiSocket.go Normal file
View File

@@ -0,0 +1,90 @@
/*
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 (
"bytes"
"fmt"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/config/configfile"
)
// --use-api-socket is not actually supported by the Docker Engine
// but is a client-side hack (see https://github.com/docker/cli/blob/master/cli/command/container/create.go#L246)
// we replicate here by transforming the project model
func (s *composeService) useAPISocket(project *types.Project) (*types.Project, error) {
useAPISocket := false
for _, service := range project.Services {
if service.UseAPISocket {
useAPISocket = true
break
}
}
if !useAPISocket {
return project, nil
}
socket := s.dockerCli.DockerEndpoint().Host
if !strings.HasPrefix(socket, "unix://") {
return nil, fmt.Errorf("use_api_socket can only be used with unix sockets: docker endpoint %s is incompatible", socket)
}
socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
creds, err := s.dockerCli.ConfigFile().GetAllCredentials()
if err != nil {
return nil, fmt.Errorf("resolving credentials failed: %w", err)
}
newConfig := &configfile.ConfigFile{
AuthConfigs: creds,
}
var configBuf bytes.Buffer
if err := newConfig.SaveToWriter(&configBuf); err != nil {
return nil, fmt.Errorf("saving creds for API socket: %w", err)
}
project.Configs["#apisocket"] = types.ConfigObjConfig{
Content: configBuf.String(),
}
for name, service := range project.Services {
service.Volumes = append(service.Volumes, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind,
Source: socket,
Target: "/var/run/docker.sock",
})
_, envvarPresent := service.Environment["DOCKER_CONFIG"]
// If the DOCKER_CONFIG env var is already present, we assume the client knows
// what they're doing and don't inject the creds.
if !envvarPresent {
// Set our special little location for the config file.
path := "/run/secrets/docker"
service.Environment["DOCKER_CONFIG"] = &path
}
service.Configs = append(service.Configs, types.ServiceConfigObjConfig{
Source: "#apisocket",
Target: "/run/secrets/docker/config.json",
})
project.Services[name] = service
}
return project, nil
}

View File

@@ -22,19 +22,16 @@ import (
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/docker/buildx/build"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/store/storeutil"
"github.com/docker/buildx/util/buildflags"
xprogress "github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/hints"
cliopts "github.com/docker/cli/opts"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
@@ -71,10 +68,6 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
}, s.stdinfo(), "Building")
}
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]api.ImageSummary) (map[string]string, error) {
imageIDs := map[string]string{}
@@ -85,7 +78,22 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
policy = types.IncludeDependencies
}
err := project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
var err error
if len(options.Services) > 0 {
// As user requested some services to be built, also include those used as additional_contexts
options.Services = addBuildDependencies(options.Services, project)
// Some build dependencies we just introduced may not be enabled
project, err = project.WithServicesEnabled(options.Services...)
if err != nil {
return nil, err
}
}
project, err = project.WithSelectedServices(options.Services)
if err != nil {
return nil, err
}
err = project.ForEachService(options.Services, func(serviceName string, service *types.ServiceConfig) error {
if service.Build == nil {
return nil
}
@@ -141,11 +149,6 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
w *xprogress.Printer
)
if buildkitEnabled {
if hints.Enabled() && progress.Mode != progress.ModeQuiet && progress.Mode != progress.ModeJSON {
suggest.Do(func() {
fmt.Fprintln(s.dockerCli.Out(), bakeSuggest) //nolint:errcheck
})
}
builderName := options.Builder
if builderName == "" {
builderName = os.Getenv("BUILDX_BUILDER")
@@ -353,6 +356,7 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
Variant: inspect.Variant,
}
if !platforms.NewMatcher(platform).Match(actual) {
logrus.Debugf("local image %s doesn't match expected platform %s", service.Image, service.Platform)
// 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
@@ -466,6 +470,11 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
return build.Options{}, err
}
attests := map[string]*string{}
if !options.Provenance {
attests["provenance"] = nil
}
return build.Options{
Inputs: build.Inputs{
ContextPath: service.Build.Context,
@@ -473,8 +482,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
NamedContexts: toBuildContexts(service, project),
},
CacheFrom: pb.CreateCaches(cacheFrom.ToPB()),
CacheTo: pb.CreateCaches(cacheTo.ToPB()),
CacheFrom: build.CreateCaches(cacheFrom),
CacheTo: build.CreateCaches(cacheTo),
NoCache: service.Build.NoCache,
Pull: service.Build.Pull,
BuildArgs: flatten(resolveAndMergeBuildArgs(s.dockerCli, project, service, options)),
@@ -489,6 +498,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
Session: sessionConfig,
Allow: allow,
SourcePolicy: sp,
Attests: attests,
}, nil
}
@@ -613,3 +623,25 @@ func parsePlatforms(service types.ServiceConfig) ([]specs.Platform, error) {
return ret, nil
}
func addBuildDependencies(services []string, project *types.Project) []string {
servicesWithDependencies := utils.NewSet(services...)
for _, service := range services {
s, ok := project.Services[service]
if !ok {
s = project.DisabledServices[service]
}
b := s.Build
if b != nil {
for _, target := range b.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()
}

View File

@@ -23,6 +23,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
@@ -51,12 +52,7 @@ import (
func buildWithBake(dockerCli command.Cli) (bool, error) {
b, ok := os.LookupEnv("COMPOSE_BAKE")
if !ok {
if dockerCli.ConfigFile().Plugins["compose"]["build"] == "bake" {
b, ok = "true", true
}
}
if !ok {
return false, nil
b = "true"
}
bake, err := strconv.ParseBool(b)
if err != nil {
@@ -72,6 +68,7 @@ func buildWithBake(dockerCli command.Cli) (bool, error) {
}
if !enabled {
logrus.Warnf("Docker Compose is configured to build using Bake, but buildkit isn't enabled")
return false, nil
}
_, err = manager.GetPlugin("buildx", dockerCli, &cobra.Command{})
@@ -117,6 +114,7 @@ type bakeTarget struct {
Ulimits []string `json:"ulimits,omitempty"`
Call string `json:"call,omitempty"`
Entitlements []string `json:"entitlements,omitempty"`
ExtraHosts map[string]string `json:"extra-hosts,omitempty"`
Outputs []string `json:"output,omitempty"`
}
@@ -124,6 +122,7 @@ type bakeMetadata map[string]buildStatus
type buildStatus struct {
Digest string `json:"containerimage.digest"`
Image string `json:"image.name"`
}
func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
@@ -142,11 +141,26 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
Groups: map[string]bakeGroup{},
Targets: map[string]bakeTarget{},
}
var group bakeGroup
var privileged bool
var read []string
var (
group bakeGroup
privileged bool
read []string
targets = make(map[string]string, len(serviceToBeBuild)) // service name -> build target
)
for serviceName, service := range serviceToBeBuild {
// produce a unique ID for service used as bake target
for serviceName := range project.Services {
t := strings.ReplaceAll(serviceName, ".", "_")
for {
if _, ok := targets[serviceName]; !ok {
targets[serviceName] = t
break
}
t += "_"
}
}
for serviceName, service := range project.Services {
if service.Build == nil {
continue
}
@@ -160,8 +174,6 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
args[k] = *v
}
image := api.GetImageNameOrDefault(service, project.Name)
entitlements := build.Entitlements
if slices.Contains(build.Entitlements, "security.insecure") {
privileged = true
@@ -171,12 +183,16 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
privileged = true
}
var output string
var outputs []string
var call string
push := options.Push && service.Image != ""
if len(service.Build.Platforms) > 1 {
output = fmt.Sprintf("type=image,push=%t", push)
} else {
output = fmt.Sprintf("type=docker,load=true,push=%t", push)
switch {
case options.Check:
call = "lint"
case len(service.Build.Platforms) > 1:
outputs = []string{fmt.Sprintf("type=image,push=%t", push)}
default:
outputs = []string{fmt.Sprintf("type=docker,load=true,push=%t", push)}
}
read = append(read, build.Context)
@@ -187,14 +203,15 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
}
}
cfg.Targets[serviceName] = bakeTarget{
target := targets[serviceName]
cfg.Targets[target] = bakeTarget{
Context: build.Context,
Contexts: additionalContexts(build.AdditionalContexts),
Contexts: additionalContexts(build.AdditionalContexts, targets),
Dockerfile: dockerFilePath(build.Context, build.Dockerfile),
DockerfileInline: strings.ReplaceAll(build.DockerfileInline, "${", "$${"),
Args: args,
Labels: build.Labels,
Tags: append(build.Tags, image),
Tags: append(build.Tags, api.GetImageNameOrDefault(service, project.Name)),
CacheFrom: build.CacheFrom,
// CacheTo: TODO
@@ -207,9 +224,19 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
ShmSize: build.ShmSize,
Ulimits: toBakeUlimits(build.Ulimits),
Entitlements: entitlements,
Outputs: []string{output},
ExtraHosts: toBakeExtraHosts(build.ExtraHosts),
Outputs: outputs,
Call: call,
}
group.Targets = append(group.Targets, serviceName)
}
// create a bake group with targets for services to build
for serviceName, service := range serviceToBeBuild {
if service.Build == nil {
continue
}
group.Targets = append(group.Targets, targets[serviceName])
}
cfg.Groups["default"] = group
@@ -220,22 +247,30 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
}
if options.Print {
_, err = fmt.Fprintln(s.stdinfo(), string(b))
_, err = fmt.Fprintln(s.stdout(), string(b))
return nil, err
}
logrus.Debugf("bake build config:\n%s", string(b))
metadata, err := os.CreateTemp(os.TempDir(), "compose")
if err != nil {
return nil, err
var metadataFile string
for {
// we don't use os.CreateTemp here as we need a temporary file name, but don't want it actually created
// as bake relies on atomicwriter and this creates conflict during rename
metadataFile = filepath.Join(os.TempDir(), fmt.Sprintf("compose-build-metadataFile-%d.json", rand.Int31()))
if _, err = os.Stat(metadataFile); os.IsNotExist(err) {
break
}
}
defer func() {
_ = os.Remove(metadataFile)
}()
buildx, err := manager.GetPlugin("buildx", s.dockerCli, &cobra.Command{})
if err != nil {
return nil, err
}
args := []string{"bake", "--file", "-", "--progress", "rawjson", "--metadata-file", metadata.Name()}
args := []string{"bake", "--file", "-", "--progress", "rawjson", "--metadata-file", metadataFile}
mustAllow := buildx.Version != "" && versions.GreaterThanOrEqualTo(buildx.Version[1:], "0.17.0")
if mustAllow {
// FIXME we should prompt user about this, but this is a breaking change in UX
@@ -250,6 +285,9 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
if options.Builder != "" {
args = append(args, "--builder", options.Builder)
}
if options.Quiet {
args = append(args, "--progress=quiet")
}
logrus.Debugf("Executing bake with args: %v", args)
@@ -259,9 +297,8 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
// Use docker/cli mechanism to propagate termination signal to child process
server, err := socket.NewPluginServer(nil)
if err != nil {
if err == nil {
defer server.Close() //nolint:errcheck
cmd.Cancel = server.Close
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
}
@@ -279,7 +316,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
return nil, err
}
var errMessage string
var errMessage []string
scanner := bufio.NewScanner(pipe)
scanner.Split(bufio.ScanLines)
@@ -295,7 +332,9 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
err := decoder.Decode(&status)
if err != nil {
if strings.HasPrefix(line, "ERROR: ") {
errMessage = line[7:]
errMessage = append(errMessage, line[7:])
} else {
errMessage = append(errMessage, line)
}
continue
}
@@ -305,13 +344,13 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
err = eg.Wait()
if err != nil {
if errMessage != "" {
return nil, errors.New(errMessage)
if len(errMessage) > 0 {
return nil, errors.New(strings.Join(errMessage, "\n"))
}
return nil, fmt.Errorf("failed to execute bake: %w", err)
}
b, err = os.ReadFile(metadata.Name())
b, err = os.ReadFile(metadataFile)
if err != nil {
return nil, err
}
@@ -324,18 +363,31 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
cw := progress.ContextWriter(ctx)
results := map[string]string{}
for name, m := range md {
results[name] = m.Digest
for name := range serviceToBeBuild {
target := targets[name]
built, ok := md[target]
if !ok {
return nil, fmt.Errorf("build result not found in Bake metadata for service %s", name)
}
results[name] = built.Digest
cw.Event(progress.BuiltEvent(name))
}
return results, nil
}
func additionalContexts(contexts types.Mapping) map[string]string {
func toBakeExtraHosts(hosts types.HostsList) map[string]string {
m := make(map[string]string)
for k, v := range hosts {
m[k] = strings.Join(v, ",")
}
return m
}
func additionalContexts(contexts types.Mapping, targets map[string]string) map[string]string {
ac := map[string]string{}
for k, v := range contexts {
if target, found := strings.CutPrefix(v, types.ServicePrefix); found {
v = "target:" + target
v = "target:" + targets[target]
}
ac[k] = v
}
@@ -400,8 +452,15 @@ func dockerFilePath(ctxName string, dockerfile string) string {
if dockerfile == "" {
return ""
}
if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
if urlutil.IsGitURL(ctxName) {
return dockerfile
}
return filepath.Join(ctxName, dockerfile)
if !filepath.IsAbs(dockerfile) {
dockerfile = filepath.Join(ctxName, dockerfile)
}
symlinks, err := filepath.EvalSymlinks(dockerfile)
if err == nil {
return symlinks
}
return dockerfile
}

View File

@@ -27,23 +27,19 @@ import (
"runtime"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/registry"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/compose/v2/pkg/api"
buildtypes "github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/builder/remotecontext/urlutil"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/moby/go-archive"
"github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus"
)
@@ -179,7 +175,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj
imageID := ""
aux := func(msg jsonmessage.JSONMessage) {
var result dockertypes.BuildResult
var result buildtypes.Result
if err := json.Unmarshal(*msg.Aux, &result); err != nil {
logrus.Errorf("Failed to parse aux message: %s", err)
} else {
@@ -219,10 +215,10 @@ func isLocalDir(c string) bool {
return err == nil
}
func imageBuildOptions(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, options api.BuildOptions) dockertypes.ImageBuildOptions {
func imageBuildOptions(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, options api.BuildOptions) buildtypes.ImageBuildOptions {
config := service.Build
return dockertypes.ImageBuildOptions{
Version: dockertypes.BuilderV1,
return buildtypes.ImageBuildOptions{
Version: buildtypes.BuilderV1,
Tags: config.Tags,
NoCache: config.NoCache,
Remove: true,

View File

@@ -69,7 +69,7 @@ func (s *composeService) commit(ctx context.Context, projectName string, options
Reference: options.Reference,
Comment: options.Comment,
Author: options.Author,
Changes: options.Changes.GetAll(),
Changes: options.Changes.GetSlice(),
Pause: options.Pause,
})
if err != nil {

View File

@@ -19,12 +19,12 @@ package compose
import (
"context"
"fmt"
"slices"
"sort"
"strconv"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
)
@@ -124,7 +124,7 @@ func matches(c container.Summary, predicates ...containerPredicate) bool {
func isService(services ...string) containerPredicate {
return func(c container.Summary) bool {
service := c.Labels[api.ServiceLabel]
return utils.StringContains(services, service)
return slices.Contains(services, service)
}
}
@@ -145,7 +145,7 @@ func isOrphaned(project *types.Project) containerPredicate {
}
// Service that is not defined in the compose model
service := c.Labels[api.ServiceLabel]
return !utils.StringContains(services, service)
return !slices.Contains(services, service)
}
}

View File

@@ -101,7 +101,7 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options
return tracing.SpanWrapFunc("service/apply", tracing.ServiceOptions(service), func(ctx context.Context) error {
strategy := options.RecreateDependencies
if utils.StringContains(options.Services, name) {
if slices.Contains(options.Services, name) {
strategy = options.Recreate
}
return c.ensureService(ctx, project, service, strategy, options.Inherit, options.Timeout)
@@ -191,7 +191,6 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
case ContainerCreated:
case ContainerRestarting:
case ContainerExited:
w.Event(progress.CreatedEvent(name))
default:
container := container
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/start", tracing.ContainerOptions(container), func(ctx context.Context) error {
@@ -595,6 +594,13 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro
w.Event(progress.CreatingEvent(eventName))
ctr, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts, w)
if err != nil {
if ctx.Err() == nil {
w.Event(progress.Event{
ID: eventName,
Status: progress.Error,
StatusText: err.Error(),
})
}
return
}
w.Event(progress.CreatedEvent(eventName))
@@ -603,10 +609,19 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro
func (s *composeService) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
replaced containerType.Summary, inherit bool, timeout *time.Duration,
) (containerType.Summary, error) {
var created containerType.Summary
) (created containerType.Summary, err error) {
w := progress.ContextWriter(ctx)
w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Working, "Recreate"))
eventName := getContainerProgressName(replaced)
w.Event(progress.NewEvent(eventName, progress.Working, "Recreate"))
defer func() {
if err != nil && ctx.Err() == nil {
w.Event(progress.Event{
ID: eventName,
Status: progress.Error,
StatusText: err.Error(),
})
}
}()
number, err := strconv.Atoi(replaced.Labels[api.ContainerNumberLabel])
if err != nil {
@@ -646,13 +661,18 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
return created, err
}
w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Done, "Recreated"))
w.Event(progress.NewEvent(eventName, progress.Done, "Recreated"))
return created, err
}
// force sequential calls to ContainerStart to prevent race condition in engine assigning ports from ranges
var startMx sync.Mutex
func (s *composeService) startContainer(ctx context.Context, ctr containerType.Summary) error {
w := progress.ContextWriter(ctx)
w.Event(progress.NewEvent(getContainerProgressName(ctr), progress.Working, "Restart"))
startMx.Lock()
defer startMx.Unlock()
err := s.apiClient().ContainerStart(ctx, ctr.ID, containerType.StartOptions{})
if err != nil {
return err

View File

@@ -78,6 +78,10 @@ func (s *composeService) ToMobyHealthCheck(ctx context.Context, check *compose.H
} else {
startInterval = time.Duration(*check.StartInterval)
}
if check.StartPeriod == nil {
// see https://github.com/moby/moby/issues/48874
return nil, errors.New("healthcheck.start_interval requires healthcheck.start_period to be set")
}
}
return &container.HealthConfig{
Test: test,

View File

@@ -22,20 +22,15 @@ import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"slices"
"strconv"
"strings"
"github.com/compose-spec/compose-go/v2/paths"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/desktop"
pathutil "github.com/docker/compose/v2/internal/paths"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
"github.com/docker/compose/v2/pkg/utils"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/docker/api/types/blkiodev"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
@@ -44,10 +39,13 @@ import (
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/versions"
volumetypes "github.com/docker/docker/api/types/volume"
"github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat"
"github.com/sirupsen/logrus"
cdi "tags.cncf.io/container-device-interface/pkg/parser"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
)
type createOptions struct {
@@ -116,6 +114,13 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
"--remove-orphans flag to clean it up.", orphans.names())
}
}
// Temporary implementation of use_api_socket until we get actual support inside docker engine
project, err = s.useAPISocket(project)
if err != nil {
return err
}
return newConvergence(options.Services, observedState, networks, volumes, s).apply(ctx, project, options)
}
@@ -155,61 +160,10 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
ids[k] = id
}
err := func() error {
if s.manageDesktopFileSharesEnabled(ctx) {
// collect all the bind mount paths and try to set up file shares in
// Docker Desktop for them
var paths []string
for _, svcName := range project.ServiceNames() {
svc := project.Services[svcName]
for _, vol := range svc.Volumes {
if vol.Type != string(mount.TypeBind) {
continue
}
p := filepath.Clean(vol.Source)
if !filepath.IsAbs(p) {
return fmt.Errorf("file share path is not absolute: %s", p)
}
if fi, err := os.Stat(p); errors.Is(err, fs.ErrNotExist) {
// actual directory will be implicitly created when the
// file share is initialized if it doesn't exist, so
// need to filter out any that should not be auto-created
if vol.Bind != nil && !vol.Bind.CreateHostPath {
logrus.Debugf("Skipping creating file share for %q: does not exist and `create_host_path` is false", p)
continue
}
} else if err != nil {
// if we can't read the path, we won't be able to make
// a file share for it
logrus.Debugf("Skipping creating file share for %q: %v", p, err)
continue
} else if !fi.IsDir() {
// ignore files & special types (e.g. Unix sockets)
logrus.Debugf("Skipping creating file share for %q: not a directory", p)
continue
}
paths = append(paths, p)
}
}
// remove duplicate/unnecessary child paths and sort them for predictability
paths = pathutil.EncompassingPaths(paths)
sort.Strings(paths)
fileShareManager := desktop.NewFileShareManager(s.desktopCli, project.Name, paths)
if err := fileShareManager.EnsureExists(ctx); err != nil {
return fmt.Errorf("initializing file shares: %w", err)
}
}
return nil
}()
if err != nil {
progress.ContextWriter(ctx).TailMsgf("Failed to prepare Synchronized file shares: %v", err)
}
return ids, nil
}
//nolint:gocyclo
func (s *composeService) getCreateConfigs(ctx context.Context,
p *types.Project,
service types.ServiceConfig,
@@ -302,7 +256,10 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
if err != nil {
return createConfigs{}, err
}
networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases, apiVersion)
networkMode, networkingConfig, err := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases, apiVersion)
if err != nil {
return createConfigs{}, err
}
portBindings := buildContainerPortBindingOptions(service)
// MISC
@@ -412,7 +369,7 @@ func (s *composeService) prepareContainerMACAddress(ctx context.Context, service
}
if len(withMacAddress) > 1 {
return "", fmt.Errorf("a MAC address is specified for multiple networks (%s), but this feature requires Docker Engine 1.44 or later (currently: %s)", strings.Join(withMacAddress, ", "), version)
return "", fmt.Errorf("a MAC address is specified for multiple networks (%s), but this feature requires Docker Engine v25 or later", strings.Join(withMacAddress, ", "))
}
if mainNw != nil && mainNw.MacAddress != "" {
@@ -435,6 +392,8 @@ func getAliases(project *types.Project, service types.ServiceConfig, serviceInde
}
func createEndpointSettings(p *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, links []string, useNetworkAliases bool) *network.EndpointSettings {
const ifname = "com.docker.network.endpoint.ifname"
config := service.Networks[networkKey]
var ipam *network.EndpointIPAMConfig
var (
@@ -454,6 +413,15 @@ func createEndpointSettings(p *types.Project, service types.ServiceConfig, servi
}
macAddress = config.MacAddress
driverOpts = config.DriverOpts
if config.InterfaceName != "" {
if driverOpts == nil {
driverOpts = map[string]string{}
}
if name, ok := driverOpts[ifname]; ok && name != config.InterfaceName {
logrus.Warnf("ignoring services.%s.networks.%s.interface_name as %s driver_opts is already declared", service.Name, networkKey, ifname)
}
driverOpts[ifname] = config.InterfaceName
}
gwPriority = config.GatewayPriority
}
return &network.EndpointSettings{
@@ -527,20 +495,17 @@ func (s *composeService) prepareLabels(labels types.Labels, service types.Servic
}
// defaultNetworkSettings determines the container.NetworkMode and corresponding network.NetworkingConfig (nil if not applicable).
func defaultNetworkSettings(
project *types.Project,
service types.ServiceConfig,
serviceIndex int,
links []string,
useNetworkAliases bool,
func defaultNetworkSettings(project *types.Project,
service types.ServiceConfig, serviceIndex int,
links []string, useNetworkAliases bool,
version string,
) (container.NetworkMode, *network.NetworkingConfig) {
) (container.NetworkMode, *network.NetworkingConfig, error) {
if service.NetworkMode != "" {
return container.NetworkMode(service.NetworkMode), nil
return container.NetworkMode(service.NetworkMode), nil, nil
}
if len(project.Networks) == 0 {
return "none", nil
return "none", nil, nil
}
var primaryNetworkKey string
@@ -571,6 +536,14 @@ func defaultNetworkSettings(
}
}
if versions.LessThan(version, "1.49") {
for _, config := range service.Networks {
if config != nil && config.InterfaceName != "" {
return "", nil, fmt.Errorf("interface_name requires Docker Engine v28.1 or later")
}
}
}
endpointsConfig[primaryNetworkMobyNetworkName] = primaryNetworkEndpoint
networkConfig := &network.NetworkingConfig{
EndpointsConfig: endpointsConfig,
@@ -579,7 +552,7 @@ func defaultNetworkSettings(
// From the Engine API docs:
// > Supported standard values are: bridge, host, none, and container:<name|id>.
// > Any other value is taken as a custom network's name to which this container should connect to.
return container.NetworkMode(primaryNetworkMobyNetworkName), networkConfig
return container.NetworkMode(primaryNetworkMobyNetworkName), networkConfig, nil
}
func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
@@ -1162,28 +1135,22 @@ func isUnixAbs(p string) bool {
}
func isWindowsAbs(p string) bool {
if strings.HasPrefix(p, "\\\\") {
return true
}
if len(p) > 2 && p[1] == ':' {
return p[2] == '\\'
}
return false
return paths.IsWindowsAbs(p)
}
func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
source := volume.Source
// on windows, filepath.IsAbs(source) is false for unix style abs path like /var/run/docker.sock.
// do not replace these with filepath.Abs(source) that will include a default drive.
if volume.Type == types.VolumeTypeBind && !filepath.IsAbs(source) && !strings.HasPrefix(source, "/") {
// volume source has already been prefixed with workdir if required, by compose-go project loader
var err error
source, err = filepath.Abs(source)
if err != nil {
return mount.Mount{}, err
switch volume.Type {
case types.VolumeTypeBind:
if !filepath.IsAbs(source) && !isUnixAbs(source) && !isWindowsAbs(source) {
// volume source has already been prefixed with workdir if required, by compose-go project loader
var err error
source, err = filepath.Abs(source)
if err != nil {
return mount.Mount{}, err
}
}
}
if volume.Type == types.VolumeTypeVolume {
case types.VolumeTypeVolume:
if volume.Source != "" {
pVolume, ok := project.Volumes[volume.Source]
if ok {
@@ -1294,7 +1261,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
}
id, err := s.resolveOrCreateNetwork(ctx, project, name, n)
if errdefs.IsConflict(err) {
if cerrdefs.IsConflict(err) {
// Maybe another execution of `docker compose up|run` created same network
// let's retry once
return s.resolveOrCreateNetwork(ctx, project, name, n)
@@ -1303,6 +1270,9 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
}
func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (string, error) { //nolint:gocyclo
// This is containers that could be left after a diverged network was removed
var dangledContainers Containers
// First, try to find a unique network matching by name or ID
inspect, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
if err == nil {
@@ -1336,7 +1306,7 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
return inspect.ID, nil
}
err = s.removeDivergedNetwork(ctx, project, name, n)
dangledContainers, err = s.removeDivergedNetwork(ctx, project, name, n)
if err != nil {
return "", err
}
@@ -1353,8 +1323,8 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
}
// NetworkList Matches all or part of a network name, so we have to filter for a strict match
networks = utils.Filter(networks, func(net network.Summary) bool {
return net.Name == n.Name
networks = slices.DeleteFunc(networks, func(net network.Summary) bool {
return net.Name != n.Name
})
for _, net := range networks {
@@ -1433,10 +1403,16 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
return "", fmt.Errorf("failed to create network %s: %w", n.Name, err)
}
w.Event(progress.CreatedEvent(networkEventName))
err = s.connectNetwork(ctx, n.Name, dangledContainers, nil)
if err != nil {
return "", err
}
return resp.ID, nil
}
func (s *composeService) removeDivergedNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) error {
func (s *composeService) removeDivergedNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (Containers, error) {
// Remove services attached to this network to force recreation
var services []string
for _, service := range project.Services.Filter(func(config types.ServiceConfig) bool {
@@ -1453,13 +1429,54 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
Project: project,
})
if err != nil {
return err
return nil, err
}
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, true, services...)
if err != nil {
return nil, err
}
err = s.disconnectNetwork(ctx, n.Name, containers)
if err != nil {
return nil, err
}
err = s.apiClient().NetworkRemove(ctx, n.Name)
eventName := fmt.Sprintf("Network %s", n.Name)
progress.ContextWriter(ctx).Event(progress.RemovedEvent(eventName))
return err
return containers, err
}
func (s *composeService) disconnectNetwork(
ctx context.Context,
network string,
containers Containers,
) error {
for _, c := range containers {
err := s.apiClient().NetworkDisconnect(ctx, network, c.ID, true)
if err != nil {
return err
}
}
return nil
}
func (s *composeService) connectNetwork(
ctx context.Context,
network string,
containers Containers,
config *network.EndpointSettings,
) error {
for _, c := range containers {
err := s.apiClient().NetworkConnect(ctx, network, c.ID, config)
if err != nil {
return err
}
}
return nil
}
func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.NetworkConfig) (string, error) {
@@ -1477,18 +1494,19 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
if len(networks) == 0 {
// in this instance, n.Name is really an ID
sn, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
if err != nil && !errdefs.IsNotFound(err) {
if err == nil {
networks = append(networks, sn)
} else if !cerrdefs.IsNotFound(err) {
return "", err
}
networks = append(networks, sn)
}
// NetworkList API doesn't return the exact name match, so we can retrieve more than one network with a request
networks = utils.Filter(networks, func(net network.Inspect) bool {
// later in this function, the name is changed the to ID.
networks = slices.DeleteFunc(networks, func(net network.Inspect) bool {
// this function is called during the rebuild stage of `compose watch`.
// we still require just one network back, but we need to run the search on the ID
return net.Name == n.Name || net.ID == n.Name
return net.Name != n.Name && net.ID != n.Name
})
switch len(networks) {
@@ -1515,7 +1533,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project, assumeYes bool) (string, error) {
inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
if err != nil {
if !errdefs.IsNotFound(err) {
if !cerrdefs.IsNotFound(err) {
return "", err
}
if volume.External {

View File

@@ -219,7 +219,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
}),
}
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "myProject_myNetwork2")
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork2"))
@@ -247,7 +248,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
}),
}
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "myProject_default")
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_default"))
@@ -264,7 +266,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
},
}
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "none")
assert.Check(t, cmp.Nil(networkConfig))
})
@@ -284,7 +287,8 @@ func TestDefaultNetworkSettings(t *testing.T) {
}),
}
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "host")
assert.Check(t, cmp.Nil(networkConfig))
})

View File

@@ -19,14 +19,13 @@ package compose
import (
"context"
"fmt"
"slices"
"strings"
"sync"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/utils"
)
// ServiceStatus indicates the status of a service
@@ -119,7 +118,7 @@ func WithRootNodesAndDown(nodes []string) func(*graphTraversal) {
t.ignored = map[string]struct{}{}
for k := range graph.Vertices {
if !utils.Contains(want, k) {
if !slices.Contains(want, k) {
t.ignored[k] = struct{}{}
}
}
@@ -434,7 +433,7 @@ func (g *Graph) HasCycles() (bool, error) {
path := []string{
vertex.Key,
}
if !utils.StringContains(discovered, vertex.Key) && !utils.StringContains(finished, vertex.Key) {
if !slices.Contains(discovered, vertex.Key) && !slices.Contains(finished, vertex.Key) {
var err error
discovered, finished, err = g.visit(vertex.Key, path, discovered, finished)
if err != nil {
@@ -451,11 +450,11 @@ func (g *Graph) visit(key string, path []string, discovered []string, finished [
for _, v := range g.Vertices[key].Children {
path := append(path, v.Key)
if utils.StringContains(discovered, v.Key) {
if slices.Contains(discovered, v.Key) {
return nil, nil, fmt.Errorf("cycle found: %s", strings.Join(path, " -> "))
}
if !utils.StringContains(finished, v.Key) {
if !slices.Contains(finished, v.Key) {
if _, _, err := g.visit(v.Key, path, discovered, finished); err != nil {
return nil, nil, err
}

View File

@@ -17,11 +17,8 @@
package compose
import (
"context"
"github.com/docker/compose/v2/internal/desktop"
"github.com/docker/compose/v2/internal/experimental"
"github.com/sirupsen/logrus"
)
func (s *composeService) SetDesktopClient(cli *desktop.Client) {
@@ -31,18 +28,3 @@ func (s *composeService) SetDesktopClient(cli *desktop.Client) {
func (s *composeService) SetExperiments(experiments *experimental.State) {
s.experiments = experiments
}
func (s *composeService) manageDesktopFileSharesEnabled(ctx context.Context) bool {
if !s.isDesktopIntegrationActive() {
return false
}
// synchronized file share support in Docker Desktop is dependent upon
// a variety of factors (settings, OS, etc), which this endpoint abstracts
fileSharesConfig, err := s.desktopCli.GetFileSharesConfig(ctx)
if err != nil {
logrus.Debugf("Failed to retrieve file shares config: %v", err)
return false
}
return fileSharesConfig.Active && fileSharesConfig.Compose.ManageBindMounts
}

View File

@@ -23,7 +23,7 @@ import (
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/desktop"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
@@ -31,7 +31,6 @@ import (
"github.com/docker/docker/api/types/filters"
imageapi "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/errdefs"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
)
@@ -157,13 +156,6 @@ func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.P
})
}
if s.manageDesktopFileSharesEnabled(ctx) {
ops = append(ops, func() error {
desktop.RemoveFileSharesForProject(ctx, s.desktopCli, project.Name)
return nil
})
}
return ops
}
@@ -227,7 +219,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
continue
}
nw, err := s.apiClient().NetworkInspect(ctx, net.ID, network.InspectOptions{})
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
return nil
}
@@ -241,7 +233,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
}
if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
continue
}
w.Event(progress.ErrorEvent(eventName))
@@ -269,11 +261,11 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre
w.Event(progress.NewEvent(id, progress.Done, "Removed"))
return nil
}
if errdefs.IsConflict(err) {
if cerrdefs.IsConflict(err) {
w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
return nil
}
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
return nil
}
@@ -284,7 +276,7 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
resource := fmt.Sprintf("Volume %s", id)
_, err := s.apiClient().VolumeInspect(ctx, id)
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
// Already gone
return nil
}
@@ -295,11 +287,11 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
return nil
}
if errdefs.IsConflict(err) {
if cerrdefs.IsConflict(err) {
w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
return nil
}
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
return nil
}
@@ -353,7 +345,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
w := progress.ContextWriter(ctx)
eventName := getContainerProgressName(ctr)
err := s.stopContainer(ctx, w, service, ctr, timeout)
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
w.Event(progress.RemovedEvent(eventName))
return nil
}
@@ -365,7 +357,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
Force: true,
RemoveVolumes: volumes,
})
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
if err != nil && !cerrdefs.IsNotFound(err) && !cerrdefs.IsConflict(err) {
w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
return err
}

View File

@@ -18,6 +18,7 @@ package compose
import (
"context"
"slices"
"strings"
"time"
@@ -25,7 +26,6 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
)
func (s *composeService) Events(ctx context.Context, projectName string, options api.EventsOptions) error {
@@ -47,7 +47,7 @@ func (s *composeService) Events(ctx context.Context, projectName string, options
continue
}
service := event.Actor.Attributes[api.ServiceLabel]
if len(options.Services) > 0 && !utils.StringContains(options.Services, service) {
if len(options.Services) > 0 && !slices.Contains(options.Services, service) {
continue
}

View File

@@ -19,17 +19,16 @@ package compose
import (
"context"
"fmt"
"maps"
"slices"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"golang.org/x/exp/maps"
)
func (s *composeService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
@@ -54,8 +53,11 @@ func (s *composeService) Generate(ctx context.Context, options api.GenerateOptio
if err != nil {
return nil, err
}
for _, ctr := range containersByIds {
if !utils.Contains(containers, ctr) {
if !slices.ContainsFunc(containers, func(summary container.Summary) bool {
return summary.ID == ctr.ID
}) {
containers = append(containers, ctr)
}
}

View File

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

View File

@@ -19,20 +19,23 @@ package compose
import (
"context"
"fmt"
"slices"
"strings"
"sync"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
)
func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) (map[string]api.ImageSummary, error) {
projectName = strings.ToLower(projectName)
allContainers, err := s.apiClient().ContainerList(ctx, container.ListOptions{
All: true,
@@ -45,7 +48,7 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
if len(options.Services) > 0 {
// filter service containers
for _, c := range allContainers {
if utils.StringContains(options.Services, c.Labels[api.ServiceLabel]) {
if slices.Contains(options.Services, c.Labels[api.ServiceLabel]) {
containers = append(containers, c)
}
}
@@ -53,27 +56,61 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
containers = allContainers
}
images := []string{}
for _, c := range containers {
if !utils.StringContains(images, c.Image) {
images = append(images, c.Image)
}
}
imageSummaries, err := s.getImageSummaries(ctx, images)
version, err := s.RuntimeVersion(ctx)
if err != nil {
return nil, err
}
summary := make([]api.ImageSummary, len(containers))
for i, c := range containers {
img, ok := imageSummaries[c.Image]
if !ok {
return nil, fmt.Errorf("failed to retrieve image for container %s", getCanonicalContainerName(c))
}
withPlatform := versions.GreaterThanOrEqualTo(version, "1.49")
summary[i] = img
summary[i].ContainerName = getCanonicalContainerName(c)
summary := map[string]api.ImageSummary{}
var mux sync.Mutex
eg, ctx := errgroup.WithContext(ctx)
for _, c := range containers {
eg.Go(func() error {
image, err := s.apiClient().ImageInspect(ctx, c.Image)
if err != nil {
return err
}
id := image.ID // platform-specific image ID can't be combined with image tag, see https://github.com/moby/moby/issues/49995
if withPlatform && c.ImageManifestDescriptor != nil && c.ImageManifestDescriptor.Platform != nil {
image, err = s.apiClient().ImageInspect(ctx, c.Image, client.ImageInspectWithPlatform(c.ImageManifestDescriptor.Platform))
if err != nil {
return err
}
}
var repository, tag string
ref, err := reference.ParseDockerRef(c.Image)
if err == nil {
// ParseDockerRef will reject a local image ID
repository = reference.FamiliarName(ref)
if tagged, ok := ref.(reference.Tagged); ok {
tag = tagged.Tag()
}
}
mux.Lock()
defer mux.Unlock()
summary[getCanonicalContainerName(c)] = api.ImageSummary{
ID: id,
Repository: repository,
Tag: tag,
Platform: platforms.Platform{
Architecture: image.Architecture,
OS: image.Os,
OSVersion: image.OsVersion,
Variant: image.Variant,
},
Size: image.Size,
LastTagTime: image.Metadata.LastTagTime,
}
return nil
})
}
return summary, nil
err = eg.Wait()
return summary, err
}
func (s *composeService) getImageSummaries(ctx context.Context, repoTags []string) (map[string]api.ImageSummary, error) {
@@ -84,7 +121,7 @@ func (s *composeService) getImageSummaries(ctx context.Context, repoTags []strin
eg.Go(func() error {
inspect, err := s.apiClient().ImageInspect(ctx, repoTag)
if err != nil {
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
return nil
}
return fmt.Errorf("unable to get image '%s': %w", repoTag, err)

View File

@@ -21,6 +21,7 @@ import (
"strings"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
@@ -42,9 +43,10 @@ func TestImages(t *testing.T) {
ctx := context.Background()
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)))
listOpts := container.ListOptions{All: true, Filters: args}
api.EXPECT().ServerVersion(gomock.Any()).Return(types.Version{APIVersion: "1.96"}, nil).AnyTimes()
image1 := imageInspect("image1", "foo:1", 12345)
image2 := imageInspect("image2", "bar:2", 67890)
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(image1, nil)
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(image1, nil).MaxTimes(2)
api.EXPECT().ImageInspect(anyCancellableContext(), "bar:2").Return(image2, nil)
c1 := containerDetail("service1", "123", "running", "foo:1")
c2 := containerDetail("service1", "456", "running", "bar:2")
@@ -54,27 +56,24 @@ func TestImages(t *testing.T) {
images, err := tested.Images(ctx, strings.ToLower(testProject), compose.ImagesOptions{})
expected := []compose.ImageSummary{
{
ID: "image1",
ContainerName: "123",
Repository: "foo",
Tag: "1",
Size: 12345,
expected := map[string]compose.ImageSummary{
"123": {
ID: "image1",
Repository: "foo",
Tag: "1",
Size: 12345,
},
{
ID: "image2",
ContainerName: "456",
Repository: "bar",
Tag: "2",
Size: 67890,
"456": {
ID: "image2",
Repository: "bar",
Tag: "2",
Size: 67890,
},
{
ID: "image1",
ContainerName: "789",
Repository: "foo",
Tag: "1",
Size: 12345,
"789": {
ID: "image1",
Repository: "foo",
Tag: "1",
Size: 12345,
},
}
assert.NilError(t, err)

View File

@@ -19,11 +19,11 @@ package compose
import (
"context"
"fmt"
"slices"
"sort"
"strings"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
@@ -74,7 +74,7 @@ func combinedConfigFiles(containers []container.Summary) (string, error) {
}
for _, f := range strings.Split(files, ",") {
if !utils.StringContains(configFiles, f) {
if !slices.Contains(configFiles, f) {
configFiles = append(configFiles, f)
}
}

View File

@@ -17,6 +17,7 @@
package compose
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -24,16 +25,18 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"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/cli/cli/config"
"github.com/docker/compose/v2/pkg/progress"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"golang.org/x/sync/errgroup"
)
type JsonMessage struct {
@@ -42,9 +45,11 @@ type JsonMessage struct {
}
const (
ErrorType = "error"
InfoType = "info"
SetEnvType = "setenv"
ErrorType = "error"
InfoType = "info"
SetEnvType = "setenv"
DebugType = "debug"
providerMetadataDirectory = "compose/providers"
)
func (s *composeService) runPlugin(ctx context.Context, project *types.Project, service types.ServiceConfig, command string) error {
@@ -55,67 +60,19 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
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()
cmd, err := s.setupPluginCommand(ctx, project, service, plugin, command)
if err != nil {
return err
}
err = cmd.Start()
variables, err := s.executePlugin(ctx, cmd, command, service)
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 {
prefix := strings.ToUpper(service.Name) + "_"
for key, val := range variables {
s.Environment[prefix+key] = &val
}
@@ -125,26 +82,131 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
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))
func (s *composeService) executePlugin(ctx context.Context, cmd *exec.Cmd, command string, service types.ServiceConfig) (types.Mapping, error) {
pw := progress.ContextWriter(ctx)
var action string
switch command {
case "up":
pw.Event(progress.CreatingEvent(service.Name))
action = "create"
case "down":
pw.Event(progress.RemovingEvent(service.Name))
action = "remove"
default:
return nil, fmt.Errorf("unsupported plugin command: %s", command)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
decoder := json.NewDecoder(stdout)
defer func() { _ = stdout.Close() }()
variables := types.Mapping{}
for {
var msg JsonMessage
err = decoder.Decode(&msg)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
switch msg.Type {
case ErrorType:
pw.Event(progress.NewEvent(service.Name, progress.Error, msg.Message))
return nil, errors.New(msg.Message)
case InfoType:
pw.Event(progress.NewEvent(service.Name, progress.Working, msg.Message))
case SetEnvType:
key, val, found := strings.Cut(msg.Message, "=")
if !found {
return nil, fmt.Errorf("invalid response from plugin: %s", msg.Message)
}
variables[key] = val
case DebugType:
logrus.Debugf("%s: %s", service.Name, msg.Message)
default:
return nil, fmt.Errorf("invalid response from plugin: %s", msg.Type)
}
}
err = cmd.Wait()
if err != nil {
pw.Event(progress.ErrorMessageEvent(service.Name, err.Error()))
return nil, fmt.Errorf("failed to %s service provider: %s", action, err.Error())
}
switch command {
case "up":
pw.Event(progress.CreatedEvent(service.Name))
case "down":
pw.Event(progress.RemovedEvent(service.Name))
}
return variables, nil
}
func (s *composeService) getPluginBinaryPath(provider string) (path string, err error) {
if provider == "compose" {
return "", errors.New("'compose' is not a valid provider type")
}
plugin, err := manager.GetPlugin(provider, s.dockerCli, &cobra.Command{})
if err == nil {
path = plugin.Path
}
if manager.IsNotFound(err) {
path, err = exec.LookPath(executable(provider))
}
return path, err
}
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) (*exec.Cmd, error) {
cmdOptionsMetadata := s.getPluginMetadata(path, service.Provider.Type)
var currentCommandMetadata CommandMetadata
switch command {
case "up":
currentCommandMetadata = cmdOptionsMetadata.Up
case "down":
currentCommandMetadata = cmdOptionsMetadata.Down
}
commandMetadataIsEmpty := len(currentCommandMetadata.Parameters) == 0
provider := *service.Provider
if err := currentCommandMetadata.CheckRequiredParameters(provider); !commandMetadataIsEmpty && err != nil {
return nil, err
}
args := []string{"compose", "--project-name", project.Name, command}
for k, v := range provider.Options {
for _, value := range v {
if _, ok := currentCommandMetadata.GetParameter(k); commandMetadataIsEmpty || ok {
args = append(args, fmt.Sprintf("--%s=%s", k, value))
}
}
}
args = append(args, service.Name)
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)
// exec provider command with same environment Compose is running
env := types.NewMapping(os.Environ())
// but remove DOCKER_CLI_PLUGIN... variable so plugin can detect it run standalone
delete(env, manager.ReexecEnvvar)
// and add the explicit environment variables set for service
for key, val := range service.Environment.RemoveEmpty().ToMapping() {
env[key] = val
}
cmd.Env = env.Values()
// Use docker/cli mechanism to propagate termination signal to child process
server, err := socket.NewPluginServer(nil)
if err == nil {
defer server.Close() //nolint:errcheck
cmd.Cancel = server.Close
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
}
@@ -154,26 +216,73 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, &carrier)
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
return cmd
return cmd, nil
}
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)
func (s *composeService) getPluginMetadata(path, command string) ProviderMetadata {
cmd := exec.Command(path, "compose", "metadata")
stdout := &bytes.Buffer{}
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
logrus.Debugf("failed to start plugin metadata command: %v", err)
return ProviderMetadata{}
}
// 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())
}
var metadata ProviderMetadata
if err := json.Unmarshal(stdout.Bytes(), &metadata); err != nil {
output, _ := io.ReadAll(stdout)
logrus.Debugf("failed to decode plugin metadata: %v - %s", err, output)
return ProviderMetadata{}
}
// Save metadata into docker home directory to be used by Docker LSP tool
// Just log the error as it's not a critical error for the main flow
metadataDir := filepath.Join(config.Dir(), providerMetadataDirectory)
if err := os.MkdirAll(metadataDir, 0o700); err == nil {
metadataFilePath := filepath.Join(metadataDir, command+".json")
if err := os.WriteFile(metadataFilePath, stdout.Bytes(), 0o600); err != nil {
logrus.Debugf("failed to save plugin metadata: %v", err)
}
} else {
return fmt.Errorf("unsupported provider %q", plugin.Name)
logrus.Debugf("failed to create plugin metadata directory: %v", err)
}
return metadata
}
type ProviderMetadata struct {
Description string `json:"description"`
Up CommandMetadata `json:"up"`
Down CommandMetadata `json:"down"`
}
type CommandMetadata struct {
Parameters []ParameterMetadata `json:"parameters"`
}
type ParameterMetadata struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}
func (c CommandMetadata) GetParameter(paramName string) (ParameterMetadata, bool) {
for _, p := range c.Parameters {
if p.Name == paramName {
return p, true
}
}
return ParameterMetadata{}, false
}
func (c CommandMetadata) CheckRequiredParameters(provider types.ServiceProviderConfig) error {
for _, p := range c.Parameters {
if p.Required {
if _, ok := provider.Options[p.Name]; !ok {
return fmt.Errorf("required parameter %q is missing from provider %q definition", p.Name, provider.Type)
}
}
}
return nil
}

View File

@@ -0,0 +1,23 @@
//go:build windows
/*
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
func executable(s string) string {
return s + ".exe"
}

View File

@@ -24,6 +24,7 @@ import (
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
@@ -203,6 +204,15 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
Platform: platform,
})
if ctx.Err() != nil {
w.Event(progress.Event{
ID: service.Name,
Status: progress.Warning,
StatusText: "Interrupted",
})
return "", nil
}
// check if has error and the service has a build section
// then the status should be warning instead of error
if err != nil && service.Build != nil {
@@ -322,9 +332,12 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types.
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(s.maxConcurrency)
pulledImages := map[string]api.ImageSummary{}
var mutex sync.Mutex
for name, service := range needPull {
eg.Go(func() error {
id, err := s.pullServiceImage(ctx, service, s.configFile(), w, quietPull, project.Environment["DOCKER_DEFAULT_PLATFORM"])
mutex.Lock()
defer mutex.Unlock()
pulledImages[name] = api.ImageSummary{
ID: id,
Repository: service.Image,

View File

@@ -62,7 +62,7 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
w := progress.ContextWriter(ctx)
for _, service := range project.Services {
if service.Build == nil || service.Image == "" {
if options.ImageMandatory && service.Image == "" {
if options.ImageMandatory && service.Image == "" && service.Provider == nil {
return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name)
}
w.Event(progress.Event{

View File

@@ -22,12 +22,13 @@ import (
"fmt"
"os"
"os/signal"
"slices"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
cmd "github.com/docker/cli/cli/command/container"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/pkg/stringid"
)
@@ -58,6 +59,19 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
}
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
// Temporary implementation of use_api_socket until we get actual support inside docker engine
project, err := s.useAPISocket(project)
if err != nil {
return "", err
}
err = progress.Run(ctx, func(ctx context.Context) error {
return s.startDependencies(ctx, project, opts)
}, s.stdinfo())
if err != nil {
return "", err
}
service, err := project.GetService(opts.Service)
if err != nil {
return "", err
@@ -130,11 +144,11 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
if len(opts.CapAdd) > 0 {
service.CapAdd = append(service.CapAdd, opts.CapAdd...)
service.CapDrop = utils.Remove(service.CapDrop, opts.CapAdd...)
service.CapDrop = slices.DeleteFunc(service.CapDrop, func(e string) bool { return slices.Contains(opts.CapAdd, e) })
}
if len(opts.CapDrop) > 0 {
service.CapDrop = append(service.CapDrop, opts.CapDrop...)
service.CapAdd = utils.Remove(service.CapAdd, opts.CapDrop...)
service.CapAdd = slices.DeleteFunc(service.CapAdd, func(e string) bool { return slices.Contains(opts.CapDrop, e) })
}
if opts.WorkingDir != "" {
service.WorkingDir = opts.WorkingDir
@@ -160,3 +174,33 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
service.Labels = service.Labels.Add(k, v)
}
}
func (s *composeService) startDependencies(ctx context.Context, project *types.Project, options api.RunOptions) error {
var dependencies []string
for name := range project.Services {
if name != options.Service {
dependencies = append(dependencies, name)
}
}
project, err := project.WithSelectedServices(dependencies)
if err != nil {
return err
}
err = s.Create(ctx, project, api.CreateOptions{
Build: options.Build,
IgnoreOrphans: options.IgnoreOrphans,
RemoveOrphans: options.RemoveOrphans,
QuietPull: options.QuietPull,
})
if err != nil {
return err
}
if len(dependencies) > 0 {
return s.Start(ctx, project.Name, api.StartOptions{
Project: project,
})
}
return nil
}

View File

@@ -20,15 +20,15 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"time"
containerType "github.com/docker/docker/api/types/container"
"github.com/docker/docker/errdefs"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
containerType "github.com/docker/docker/api/types/container"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/docker/api/types/filters"
@@ -199,7 +199,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
ofInterest := func(c containerType.Summary) bool {
if len(services) > 0 {
// we only watch some services
return utils.Contains(services, c.Labels[api.ServiceLabel])
return slices.Contains(services, c.Labels[api.ServiceLabel])
}
return true
}
@@ -208,7 +208,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
isRequired := func(c containerType.Summary) bool {
if len(services) > 0 && len(required) > 0 {
// we only watch some services
return utils.Contains(required, c.Labels[api.ServiceLabel])
return slices.Contains(required, c.Labels[api.ServiceLabel])
}
return true
}
@@ -237,7 +237,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
}()
inspected, err := s.apiClient().ContainerInspect(ctx, event.Container)
if err != nil {
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
// it's possible to get "destroy" or "kill" events but not
// be able to inspect in time before they're gone from the
// API, so just remove the watch without erroring
@@ -263,8 +263,8 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
}
if _, ok := watched[container.ID]; ok {
eType := api.ContainerEventStopped
if utils.Contains(replaced, container.ID) {
utils.Remove(replaced, container.ID)
if slices.Contains(replaced, container.ID) {
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
eType = api.ContainerEventRecreated
}
listener(api.ContainerEvent{
@@ -290,8 +290,8 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
}
eType := api.ContainerEventExit
if utils.Contains(replaced, container.ID) {
utils.Remove(replaced, container.ID)
if slices.Contains(replaced, container.ID) {
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
eType = api.ContainerEventRecreated
}

View File

@@ -18,11 +18,11 @@ package compose
import (
"context"
"slices"
"strings"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
)
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
@@ -51,7 +51,7 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a
w := progress.ContextWriter(ctx)
return InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
if !utils.StringContains(options.Services, service) {
if !slices.Contains(options.Services, service) {
return nil
}
serv := project.Services[service]

View File

@@ -0,0 +1,23 @@
//go:build !windows
/*
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
func executable(s string) string {
return s
}

View File

@@ -25,12 +25,12 @@ import (
"syscall"
"github.com/compose-spec/compose-go/v2/types"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/cli/cli"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/errdefs"
"github.com/eiannone/keyboard"
"github.com/hashicorp/go-multierror"
"github.com/sirupsen/logrus"
@@ -72,6 +72,12 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
var isTerminated atomic.Bool
printer := newLogPrinter(options.Start.Attach)
watcher, err := NewWatcher(project, options, s.watch)
if err != nil && options.Start.Watch {
return err
}
var navigationMenu *formatter.LogKeyboard
var kEvents <-chan keyboard.KeyEvent
if options.Start.NavigationMenu {
kEvents, err = keyboard.GetKeys(100)
@@ -80,20 +86,14 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
options.Start.NavigationMenu = false
} else {
defer keyboard.Close() //nolint:errcheck
isWatchConfigured := s.shouldWatch(project)
isDockerDesktopActive := s.isDesktopIntegrationActive()
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured)
formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, signalChan, s.watch)
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, watcher != nil)
navigationMenu = formatter.NewKeyboardManager(isDockerDesktopActive, signalChan, options.Start.Watch, watcher)
}
}
doneCh := make(chan bool)
eg.Go(func() error {
if options.Start.NavigationMenu && options.Start.Watch {
// Run watch by navigation menu, so we can interactively enable/disable
formatter.KeyboardManager.StartWatch(ctx, doneCh, project, options)
}
first := true
gracefulTeardown := func() {
printer.Cancel()
@@ -112,6 +112,9 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
for {
select {
case <-doneCh:
if watcher != nil {
return watcher.Stop()
}
return nil
case <-ctx.Done():
if first {
@@ -119,6 +122,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
}
case <-signalChan:
if first {
keyboard.Close() //nolint:errcheck
gracefulTeardown()
break
}
@@ -129,7 +133,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
All: true,
})
// Ignore errors indicating that some of the containers were already stopped or removed.
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) {
if cerrdefs.IsNotFound(err) || cerrdefs.IsConflict(err) {
return nil
}
@@ -137,7 +141,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
})
return nil
case event := <-kEvents:
formatter.KeyboardManager.HandleKeyEvents(event, ctx, doneCh, project, options)
navigationMenu.HandleKeyEvents(ctx, event, project, options)
}
}
})
@@ -157,15 +161,11 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
return err
})
if options.Start.Watch && !options.Start.NavigationMenu {
eg.Go(func() error {
buildOpts := *options.Create.Build
buildOpts.Quiet = true
return s.watch(ctx, doneCh, project, options.Start.Services, api.WatchOptions{
Build: &buildOpts,
LogTo: options.Start.Attach,
})
})
if options.Start.Watch && watcher != nil {
err = watcher.Start(ctx)
if err != nil {
return err
}
}
// We use the parent context without cancellation as we manage sigterm to stop the stack

View File

@@ -26,6 +26,7 @@ import (
"slices"
"strconv"
"strings"
gsync "sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
@@ -44,6 +45,68 @@ import (
"golang.org/x/sync/errgroup"
)
type WatchFunc func(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error)
type Watcher struct {
project *types.Project
options api.WatchOptions
watchFn WatchFunc
stopFn func()
errCh chan error
}
func NewWatcher(project *types.Project, options api.UpOptions, w WatchFunc) (*Watcher, error) {
for i := range project.Services {
service := project.Services[i]
if service.Develop != nil && service.Develop.Watch != nil {
build := options.Create.Build
build.Quiet = true
return &Watcher{
project: project,
options: api.WatchOptions{
LogTo: options.Start.Attach,
Build: build,
},
watchFn: w,
errCh: make(chan error),
}, nil
}
}
// none of the services is eligible to watch
return nil, fmt.Errorf("none of the selected services is configured for watch, see https://docs.docker.com/compose/how-tos/file-watch/")
}
// ensure state changes are atomic
var mx gsync.Mutex
func (w *Watcher) Start(ctx context.Context) error {
mx.Lock()
defer mx.Unlock()
ctx, cancelFunc := context.WithCancel(ctx)
w.stopFn = cancelFunc
wait, err := w.watchFn(ctx, w.project, w.options)
if err != nil {
return err
}
go func() {
w.errCh <- wait()
}()
return nil
}
func (w *Watcher) Stop() error {
mx.Lock()
defer mx.Unlock()
if w.stopFn == nil {
return nil
}
w.stopFn()
w.stopFn = nil
err := <-w.errCh
return err
}
// getSyncImplementation returns an appropriate sync implementation for the
// project.
//
@@ -63,20 +126,12 @@ func (s *composeService) getSyncImplementation(project *types.Project) (sync.Syn
return sync.NewTar(project.Name, tarDockerClient{s: s}), nil
}
func (s *composeService) shouldWatch(project *types.Project) bool {
var shouldWatch bool
for i := range project.Services {
service := project.Services[i]
if service.Develop != nil && service.Develop.Watch != nil {
shouldWatch = true
}
func (s *composeService) Watch(ctx context.Context, project *types.Project, options api.WatchOptions) error {
wait, err := s.watch(ctx, project, options)
if err != nil {
return err
}
return shouldWatch
}
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
return s.watch(ctx, nil, project, services, options)
return wait()
}
type watchRule struct {
@@ -127,14 +182,14 @@ func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {
}
}
func (s *composeService) watch(ctx context.Context, syncChannel chan bool, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
func (s *composeService) watch(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error) { //nolint: gocyclo
var err error
if project, err = project.WithSelectedServices(services); err != nil {
return err
if project, err = project.WithSelectedServices(options.Services); err != nil {
return nil, err
}
syncer, err := s.getSyncImplementation(project)
if err != nil {
return err
return nil, err
}
eg, ctx := errgroup.WithContext(ctx)
options.LogTo.Register(api.WatchLogger)
@@ -146,7 +201,7 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
for serviceName, service := range project.Services {
config, err := loadDevelopmentConfig(service, project)
if err != nil {
return err
return nil, err
}
if service.Develop != nil {
@@ -160,10 +215,10 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
for _, trigger := range config.Watch {
if trigger.Action == types.WatchActionRebuild {
if service.Build == nil {
return fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild)
return nil, fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild)
}
if options.Build == nil {
return fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name)
return nil, fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name)
}
// set the service to always be built - watch triggers `Up()` when it receives a rebuild event
service.PullPolicy = types.PullPolicyBuild
@@ -182,7 +237,7 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
// Need to check initial files are in container that are meant to be synched from watch action
err := s.initialSync(ctx, project, service, trigger, syncer)
if err != nil {
return err
return nil, err
}
}
}
@@ -191,45 +246,37 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
serviceWatchRules, err := getWatchRules(config, service)
if err != nil {
return err
return nil, err
}
rules = append(rules, serviceWatchRules...)
}
if len(paths) == 0 {
return fmt.Errorf("none of the selected services is configured for watch, consider setting an 'develop' section")
return nil, fmt.Errorf("none of the selected services is configured for watch, consider setting a 'develop' section")
}
watcher, err := watch.NewWatcher(paths)
if err != nil {
return err
return nil, err
}
err = watcher.Start()
if err != nil {
return err
return nil, err
}
defer func() {
if err := watcher.Close(); err != nil {
logrus.Debugf("Error closing watcher: %v", err)
}
}()
eg.Go(func() error {
return s.watchEvents(ctx, project, options, watcher, syncer, rules)
})
options.LogTo.Log(api.WatchLogger, "Watch enabled")
for {
select {
case <-ctx.Done():
return eg.Wait()
case <-syncChannel:
options.LogTo.Log(api.WatchLogger, "Watch disabled")
return nil
return func() error {
err := eg.Wait()
if werr := watcher.Close(); werr != nil {
logrus.Debugf("Error closing Watcher: %v", werr)
}
}
return err
}, nil
}
func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]watchRule, error) {
@@ -295,8 +342,13 @@ func (s *composeService) watchEvents(ctx context.Context, project *types.Project
case <-ctx.Done():
options.LogTo.Log(api.WatchLogger, "Watch disabled")
return nil
case err := <-watcher.Errors():
options.LogTo.Err(api.WatchLogger, "Watch disabled with errors")
case err, open := <-watcher.Errors():
if err != nil {
options.LogTo.Err(api.WatchLogger, "Watch disabled with errors: "+err.Error())
}
if open {
continue
}
return err
case batch := <-batchEvents:
start := time.Now()
@@ -578,7 +630,7 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se
return err
}
p, err := project.WithSelectedServices(services)
p, err := project.WithSelectedServices(services, types.IncludeDependents)
if err != nil {
return err
}

59
pkg/e2e/bridge_test.go Normal file
View File

@@ -0,0 +1,59 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"path/filepath"
"strings"
"testing"
"gotest.tools/v3/assert"
)
func TestConvertAndTransformList(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "bridge"
tmpDir := t.TempDir()
t.Run("kubernetes manifests", func(t *testing.T) {
kubedir := filepath.Join(tmpDir, "kubernetes")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/bridge/compose.yaml", "--project-name", projectName, "bridge", "convert",
"--output", kubedir)
assert.NilError(t, res.Error)
assert.Equal(t, res.ExitCode, 0)
res = c.RunCmd(t, "diff", "-r", kubedir, "./fixtures/bridge/expected-kubernetes")
assert.NilError(t, res.Error, res.Combined())
})
t.Run("helm charts", func(t *testing.T) {
helmDir := filepath.Join(tmpDir, "helm")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/bridge/compose.yaml", "--project-name", projectName, "bridge", "convert",
"--output", helmDir, "--transformation", "docker/compose-bridge-helm")
assert.NilError(t, res.Error)
assert.Equal(t, res.ExitCode, 0)
res = c.RunCmd(t, "diff", "-r", helmDir, "./fixtures/bridge/expected-helm")
assert.NilError(t, res.Error, res.Combined())
})
t.Run("list transformers images", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "bridge", "transformations",
"ls")
assert.Assert(t, strings.Contains(res.Stdout(), "docker/compose-bridge-helm"), res.Combined())
assert.Assert(t, strings.Contains(res.Stdout(), "docker/compose-bridge-kubernetes"), res.Combined())
})
}

View File

@@ -117,17 +117,22 @@ func TestLocalComposeBuild(t *testing.T) {
})
t.Run(env+" rebuild when up --build", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--workdir", "fixtures/build-test", "up", "-d", "--build")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "up", "-d", "--build")
res.Assert(t, icmd.Expected{Out: "COPY static /usr/share/nginx/html"})
res.Assert(t, icmd.Expected{Out: "COPY static2 /usr/share/nginx/html"})
})
t.Run(env+" build --push ignored for unnamed images", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--workdir", "fixtures/build-test", "build", "--push", "nginx")
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "build", "--push", "nginx")
assert.Assert(t, !strings.Contains(res.Stdout(), "failed to push"), res.Stdout())
})
t.Run(env+" build --quiet", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "build", "--quiet")
res.Assert(t, icmd.Expected{Out: ""})
})
t.Run(env+" cleanup build project", func(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test", "down")
c.RunDockerOrExitError(t, "rmi", "-f", "build-test-nginx")
@@ -232,7 +237,7 @@ func TestBuildTags(t *testing.T) {
}
func TestBuildImageDependencies(t *testing.T) {
doTest := func(t *testing.T, cli *CLI) {
doTest := func(t *testing.T, cli *CLI, args ...string) {
resetState := func() {
cli.RunDockerComposeCmd(t, "down", "--rmi=all", "-t=0")
res := cli.RunDockerOrExitError(t, "image", "rm", "build-dependencies-service")
@@ -250,7 +255,7 @@ func TestBuildImageDependencies(t *testing.T) {
Err: "No such image: build-dependencies-service",
})
res = cli.RunDockerComposeCmd(t, "build")
res = cli.RunDockerComposeCmd(t, args...)
t.Log(res.Combined())
res = cli.RunDockerCmd(t,
@@ -273,23 +278,27 @@ func TestBuildImageDependencies(t *testing.T) {
"DOCKER_BUILDKIT=0",
"COMPOSE_FILE=./fixtures/build-dependencies/classic.yaml",
))
doTest(t, cli)
doTest(t, cli, "build")
doTest(t, cli, "build", "--with-dependencies", "service")
})
t.Run("BuildKit by dependency order", func(t *testing.T) {
cli := NewCLI(t, WithEnv(
"DOCKER_BUILDKIT=1",
"DOCKER_BUILDKIT=1", "COMPOSE_BAKE=0",
"COMPOSE_FILE=./fixtures/build-dependencies/classic.yaml",
))
doTest(t, cli)
doTest(t, cli, "build")
doTest(t, cli, "build", "--with-dependencies", "service")
})
t.Run("BuildKit by additional contexts", func(t *testing.T) {
cli := NewCLI(t, WithEnv(
"DOCKER_BUILDKIT=1",
"DOCKER_BUILDKIT=1", "COMPOSE_BAKE=0",
"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
))
doTest(t, cli)
doTest(t, cli, "build")
doTest(t, cli, "build", "service")
doTest(t, cli, "up", "--build", "service")
})
t.Run("Bake by additional contexts", func(t *testing.T) {
@@ -297,7 +306,9 @@ func TestBuildImageDependencies(t *testing.T) {
"DOCKER_BUILDKIT=1", "COMPOSE_BAKE=1",
"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
))
doTest(t, cli)
doTest(t, cli, "--verbose", "build")
doTest(t, cli, "--verbose", "build", "service")
doTest(t, cli, "--verbose", "up", "--build", "service")
})
}
@@ -513,3 +524,59 @@ func TestBuildEntitlements(t *testing.T) {
}
})
}
func TestBuildDependsOn(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "--progress=plain", "up", "test2")
out := res.Combined()
assert.Check(t, strings.Contains(out, "test1 Built"))
}
func TestBuildSubset(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "build", "main")
out := res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
}
func TestBuildDependentImage(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "firstbuild")
out := res.Combined()
assert.Check(t, strings.Contains(out, "firstbuild Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "secondbuild")
out = res.Combined()
assert.Check(t, strings.Contains(out, "secondbuild Built"))
}
func TestBuildSubDependencies(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "build", "main")
out := res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "up", "--build", "main")
out = res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
}

View File

@@ -170,6 +170,18 @@ func TestLocalComposeRun(t *testing.T) {
assert.Assert(t, strings.Contains(res.Combined(), "Pulled"), res.Combined())
})
t.Run("COMPOSE_PROGRESS quiet", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "down", "--remove-orphans", "--rmi", "all")
res.Assert(t, icmd.Success)
cmd := c.NewDockerComposeCmd(t, "-f", "./fixtures/run-test/quiet-pull.yaml", "run", "backend")
res = icmd.RunCmd(cmd, func(c *icmd.Cmd) {
c.Env = append(c.Env, "COMPOSE_PROGRESS=quiet")
})
assert.Assert(t, !strings.Contains(res.Combined(), "Pull complete"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Pulled"), res.Combined())
})
t.Run("--pull", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/pull.yaml", "down", "--remove-orphans", "--rmi", "all")
res.Assert(t, icmd.Success)

View File

@@ -235,14 +235,14 @@ func TestCompatibility(t *testing.T) {
}
func TestConfig(t *testing.T) {
const projectName = "compose-e2e-convert"
const projectName = "compose-e2e-config"
c := NewParallelCLI(t)
wd, err := os.Getwd()
assert.NilError(t, err)
t.Run("up", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose.yaml", "-p", projectName, "convert")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose.yaml", "-p", projectName, "config")
res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s
services:
nginx:
@@ -253,24 +253,24 @@ services:
default: null
networks:
default:
name: compose-e2e-convert_default
name: compose-e2e-config_default
`, projectName, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0})
})
}
func TestConfigInterpolate(t *testing.T) {
const projectName = "compose-e2e-convert-interpolate"
const projectName = "compose-e2e-config-interpolate"
c := NewParallelCLI(t)
wd, err := os.Getwd()
assert.NilError(t, err)
t.Run("convert", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "convert", "--no-interpolate")
t.Run("config", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "config", "--no-interpolate")
res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`name: %s
networks:
default:
name: compose-e2e-convert-interpolate_default
name: compose-e2e-config-interpolate_default
services:
nginx:
build:

View File

@@ -46,4 +46,25 @@ func TestLocalComposeConfig(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config/compose.yaml", "--project-name", projectName, "config", "--no-interpolate")
res.Assert(t, icmd.Expected{Out: `- ${PORT:-8080}:80`})
})
t.Run("--variables --format json", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config/compose.yaml", "--project-name", projectName, "config", "--variables", "--format", "json")
res.Assert(t, icmd.Expected{Out: `{
"PORT": {
"Name": "PORT",
"DefaultValue": "8080",
"PresenceValue": "",
"Required": false
}
}`})
})
t.Run("--variables --format yaml", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/config/compose.yaml", "--project-name", projectName, "config", "--variables", "--format", "yaml")
res.Assert(t, icmd.Expected{Out: `PORT:
name: PORT
defaultvalue: "8080"
presencevalue: ""
required: false`})
})
}

View File

@@ -0,0 +1,18 @@
# 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
ENV ENV_FROM_DOCKERFILE=1
EXPOSE 8081
CMD ["echo", "Hello from Dockerfile"]

View File

@@ -0,0 +1,31 @@
services:
serviceA:
image: alpine
build: .
ports:
- 80:8080
networks:
- private-network
configs:
- source: my-config
target: /etc/my-config1.txt
serviceB:
image: alpine
build: .
ports:
- 8081:8082
secrets:
- my-secrets
networks:
- private-network
- public-network
configs:
my-config:
file: my-config.txt
secrets:
my-secrets:
file: not-so-secret.txt
networks:
private-network:
internal: true
public-network: {}

View File

@@ -0,0 +1,12 @@
#! Chart.yaml
apiVersion: v2
name: bridge
version: 0.0.1
# kubeVersion: >= 1.29.1
description: A generated Helm Chart for bridge generated via compose-bridge.
type: application
keywords:
- bridge
appVersion: 'v0.0.1'
sources:
annotations:

View File

@@ -0,0 +1,8 @@
#! 0-bridge-namespace.yaml
# Generated code, do not edit
apiVersion: v1
kind: Namespace
metadata:
name: bridge
labels:
com.docker.compose.project: bridge

View File

@@ -0,0 +1,12 @@
#! bridge-configs.yaml
# Generated code, do not edit
apiVersion: v1
kind: ConfigMap
metadata:
name: bridge
namespace: bridge
labels:
com.docker.compose.project: bridge
data:
my-config: |
My config file

View File

@@ -0,0 +1,13 @@
#! my-secrets-secret.yaml
# Generated code, do not edit
apiVersion: v1
kind: Secret
metadata:
name: my-secrets
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.secret: my-secrets
data:
my-secrets: bm90LXNlY3JldA==
type: Opaque

View File

@@ -0,0 +1,24 @@
#! private-network-network-policy.yaml
# Generated code, do not edit
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: private-network-network-policy
namespace: {{ .Values.namespace }}
spec:
podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"
egress:
- to:
- podSelector:
matchLabels:
com.docker.compose.network.private-network: "true"

View File

@@ -0,0 +1,24 @@
#! public-network-network-policy.yaml
# Generated code, do not edit
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: public-network-network-policy
namespace: {{ .Values.namespace }}
spec:
podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"
egress:
- to:
- podSelector:
matchLabels:
com.docker.compose.network.public-network: "true"

View File

@@ -0,0 +1,45 @@
#! serviceA-deployment.yaml
# Generated code, do not edit
apiVersion: apps/v1
kind: Deployment
metadata:
name: servicea
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
strategy:
type: Recreate
template:
metadata:
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
com.docker.compose.network.private-network: "true"
spec:
containers:
- name: servicea
image: {{ .Values.serviceA.image }}
imagePullPolicy: {{ .Values.serviceA.imagePullPolicy }}
ports:
- name: servicea-8080
containerPort: 8080
volumeMounts:
- name: etc-my-config1-txt
mountPath: /etc/my-config1.txt
subPath: my-config
readOnly: true
volumes:
- name: etc-my-config1-txt
configMap:
name: bridge
items:
- key: my-config
path: my-config

View File

@@ -0,0 +1,19 @@
#! serviceA-expose.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: servicea
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
app.kubernetes.io/managed-by: Helm
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
ports:
- name: servicea-8080
port: 8080
targetPort: servicea-8080

View File

@@ -0,0 +1,25 @@
# check if there is at least one published port
#! serviceA-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: servicea-published
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
app.kubernetes.io/managed-by: Helm
spec:
type: LoadBalancer
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
ports:
- name: servicea-80
port: 80
protocol: TCP
targetPort: servicea-8080
# check if there is at least one published port

View File

@@ -0,0 +1,46 @@
#! serviceB-deployment.yaml
# Generated code, do not edit
apiVersion: apps/v1
kind: Deployment
metadata:
name: serviceb
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
strategy:
type: Recreate
template:
metadata:
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
com.docker.compose.network.private-network: "true"
com.docker.compose.network.public-network: "true"
spec:
containers:
- name: serviceb
image: {{ .Values.serviceB.image }}
imagePullPolicy: {{ .Values.serviceB.imagePullPolicy }}
ports:
- name: serviceb-8082
containerPort: 8082
volumeMounts:
- name: run-secrets-my-secrets
mountPath: /run/secrets/my-secrets
subPath: my-secrets
readOnly: true
volumes:
- name: run-secrets-my-secrets
secret:
secretName: my-secrets
items:
- key: my-secrets
path: my-secrets

View File

@@ -0,0 +1,19 @@
#! serviceB-expose.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
app.kubernetes.io/managed-by: Helm
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
ports:
- name: serviceb-8082
port: 8082
targetPort: serviceb-8082

View File

@@ -0,0 +1,21 @@
#! serviceB-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb-published
namespace: {{ .Values.namespace }}
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
app.kubernetes.io/managed-by: Helm
spec:
type: LoadBalancer
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
ports:
- name: serviceb-8081
port: 8081
protocol: TCP
targetPort: serviceb-8082

View File

@@ -0,0 +1,13 @@
#! values.yaml
# Namespace
namespace: bridge
# Services variables
serviceA:
image: alpine
imagePullPolicy: IfNotPresent
serviceB:
image: alpine
imagePullPolicy: IfNotPresent
# You can apply the same logic to loop on networks, volumes, secrets and configs...

View File

@@ -0,0 +1,8 @@
#! 0-bridge-namespace.yaml
# Generated code, do not edit
apiVersion: v1
kind: Namespace
metadata:
name: bridge
labels:
com.docker.compose.project: bridge

View File

@@ -0,0 +1,12 @@
#! bridge-configs.yaml
# Generated code, do not edit
apiVersion: v1
kind: ConfigMap
metadata:
name: bridge
namespace: bridge
labels:
com.docker.compose.project: bridge
data:
my-config: |
My config file

View File

@@ -0,0 +1,16 @@
#! kustomization.yaml
# Generated code, do not edit
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- 0-bridge-namespace.yaml
- bridge-configs.yaml
- my-secrets-secret.yaml
- private-network-network-policy.yaml
- public-network-network-policy.yaml
- serviceA-deployment.yaml
- serviceA-expose.yaml
- serviceA-service.yaml
- serviceB-deployment.yaml
- serviceB-expose.yaml
- serviceB-service.yaml

View File

@@ -0,0 +1,13 @@
#! my-secrets-secret.yaml
# Generated code, do not edit
apiVersion: v1
kind: Secret
metadata:
name: my-secrets
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.secret: my-secrets
data:
my-secrets: bm90LXNlY3JldA==
type: Opaque

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