Compare commits

..

No commits in common. "main" and "v2.37.0" have entirely different histories.

143 changed files with 1316 additions and 3465 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,18 +18,13 @@ package compose
import (
"context"
"errors"
"fmt"
"os"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type execOpts struct {
@ -64,15 +59,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
err := runExec(ctx, dockerCli, backend, opts)
if err != nil {
logrus.Debugf("%v", err)
var cliError cli.StatusError
if ok := errors.As(err, &cliError); ok {
os.Exit(err.(cli.StatusError).StatusCode) //nolint: errorlint
}
}
return err
return runExec(ctx, dockerCli, backend, opts)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
@ -82,7 +69,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
runCmd.Flags().IntVar(&opts.index, "index", 0, "Index of the container if service has multiple replicas")
runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process")
runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user")
runCmd.Flags().BoolVarP(&opts.noTty, "no-tty", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command")
runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
@ -91,12 +78,6 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
runCmd.Flags().SetInterspersed(false)
runCmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
if name == "no-TTY" { // legacy
name = "no-tty"
}
return pflag.NormalizedName(name)
})
return runCmd
}
@ -128,8 +109,8 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, op
exitCode, err := backend.Exec(ctx, projectName, execOpts)
if exitCode != 0 {
errMsg := fmt.Sprintf("exit status %d", exitCode)
if err != nil && err.Error() != "" {
errMsg := ""
if err != nil {
errMsg = err.Error()
}
return cli.StatusError{StatusCode: exitCode, Status: errMsg}

View File

@ -87,41 +87,6 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
}
return nil
}
if opts.Format == "json" {
type img struct {
ID string `json:"ID"`
ContainerName string `json:"ContainerName"`
Repository string `json:"Repository"`
Tag string `json:"Tag"`
Platform string `json:"Platform"`
Size int64 `json:"Size"`
LastTagTime time.Time `json:"LastTagTime"`
}
// Convert map to slice
var imageList []img
for ctr, i := range images {
lastTagTime := i.LastTagTime
if lastTagTime.IsZero() {
lastTagTime = i.Created
}
imageList = append(imageList, img{
ContainerName: ctr,
ID: i.ID,
Repository: i.Repository,
Tag: i.Tag,
Platform: platforms.Format(i.Platform),
Size: i.Size,
LastTagTime: 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) {

View File

@ -120,8 +120,8 @@ func (options runOptions) apply(project *types.Project) (*types.Project, error)
return project, nil
}
func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (types.Mapping, error) {
environment := types.NewMappingWithEquals(options.environment).Resolve(resolve).ToMapping()
func (options runOptions) getEnvironment() (types.Mapping, error) {
environment := types.NewMappingWithEquals(options.environment).Resolve(os.LookupEnv).ToMapping()
for _, file := range options.envFiles {
f, err := os.Open(file)
if err != nil {
@ -271,6 +271,22 @@ 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)
@ -282,26 +298,23 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
var buildForRun *api.BuildOptions
if !createOpts.noBuild {
bo, err := buildOpts.toAPIBuildOptions(nil)
// 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})
if err != nil {
return err
}
buildForRun = &bo
}
environment, err := options.getEnvironment(project.Environment.Resolve)
environment, err := options.getEnvironment()
if err != nil {
return err
}
// start container and attach to container streams
runOpts := api.RunOptions{
CreateOptions: api.CreateOptions{
Build: buildForRun,
RemoveOrphans: options.removeOrphans,
IgnoreOrphans: options.ignoreOrphans,
QuietPull: options.quietPull,
},
Build: buildForRun,
Name: options.name,
Service: options.Service,
Command: options.Command,
@ -319,6 +332,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
UseNetworkAliases: options.useAliases,
NoDeps: options.noDeps,
Index: 0,
QuietPull: options.quietPull,
}
for name, service := range project.Services {
@ -338,3 +352,34 @@ 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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,16 +14,25 @@
limitations under the License.
*/
package e2e
package formatter
import (
"testing"
"strings"
"github.com/hashicorp/go-multierror"
)
func TestComposeModel(t *testing.T) {
t.Skip("waiting for docker-model release")
c := NewParallelCLI(t)
defer c.cleanupWithDown(t, "model-test")
c.RunDockerComposeCmd(t, "-f", "./fixtures/model/compose.yaml", "run", "test", "sh", "-c", "curl ${FOO_URL}")
// SetMultiErrorFormat set cli default format for multi-errors
func SetMultiErrorFormat(errs *multierror.Error) {
if errs != nil {
errs.ErrorFormat = formatErrors
}
}
func formatErrors(errs []error) string {
messages := make([]string, len(errs))
for i, err := range errs {
messages[i] = "Error: " + err.Error()
}
return strings.Join(messages, "\n")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +0,0 @@
# docker compose volumes
<!---MARKER_GEN_START-->
List volumes
### Options
| Name | Type | Default | Description |
|:----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--dry-run` | `bool` | | Execute command in dry run mode |
| `--format` | `string` | `table` | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
| `-q`, `--quiet` | `bool` | | Only display volume names |
<!---MARKER_GEN_END-->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +0,0 @@
command: docker compose volumes
short: List volumes
long: List volumes
usage: docker compose volumes [OPTIONS] [SERVICE...]
pname: docker compose
plink: docker_compose.yaml
options:
- option: format
value_type: string
default_value: table
description: |-
Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
- option: quiet
shorthand: q
value_type: bool
default_value: "false"
description: Only display volume names
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

88
go.mod
View File

@ -1,6 +1,6 @@
module github.com/docker/compose/v2
go 1.24.7
go 1.23.8
require (
github.com/AlecAivazis/survey/v2 v2.3.7
@ -8,27 +8,28 @@ 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.8.2
github.com/containerd/containerd/v2 v2.1.4
github.com/compose-spec/compose-go/v2 v2.6.4
github.com/containerd/containerd/v2 v2.1.1
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.28.0
github.com/docker/cli v28.4.0+incompatible
github.com/docker/buildx v0.24.0
github.com/docker/cli v28.2.2+incompatible
github.com/docker/cli-docs-tool v0.10.0
github.com/docker/docker v28.4.0+incompatible
github.com/docker/go-connections v0.6.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
github.com/fsnotify/fsevents v0.2.0
github.com/google/go-cmp v0.7.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.7.0
github.com/jonboulle/clockwork v0.5.0
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-ps v1.0.0
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/buildkit v0.24.0
github.com/moby/buildkit v0.22.0
github.com/moby/go-archive v0.1.0
github.com/moby/patternmatcher v0.6.0
github.com/moby/sys/atomicwriter v0.1.0
@ -39,30 +40,31 @@ require (
github.com/otiai10/copy v1.14.1
github.com/sirupsen/logrus v1.9.3
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
go.opentelemetry.io/otel v1.36.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.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/trace v1.36.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.6.0
golang.org/x/sync v0.17.0
golang.org/x/sys v0.36.0
google.golang.org/grpc v1.74.2
go.uber.org/mock v0.5.2
golang.org/x/sync v0.14.0
golang.org/x/sys v0.33.0
google.golang.org/grpc v1.72.2
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v1.0.1
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
@ -80,14 +82,14 @@ 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.5 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/containerd/api v1.9.0 // indirect
github.com/containerd/continuity v0.4.5 // 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
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
@ -96,12 +98,12 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.3 // 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.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.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
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
@ -115,8 +117,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/in-toto/in-toto-golang v0.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
github.com/josharian/intern v1.0.0 // indirect
@ -156,36 +157,37 @@ require (
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/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.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-20250605211040-586307ad452f // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250417144416-3f76f8130144 // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // 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
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/zclconf/go-cty v1.16.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect
go.opentelemetry.io/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.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // 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
@ -202,7 +204,7 @@ require (
)
exclude (
// FIXME(thaJeztah): remove this once kubernetes updated their dependencies to no longer need this.
// FIXME(thaJeztah): remoove this once kubernetes updated their dependencies to no longer need this.
//
// For additional details, see this PR and links mentioned in that PR:
// https://github.com/kubernetes-sigs/kustomize/pull/5830#issuecomment-2569960859

187
go.sum
View File

@ -1,3 +1,5 @@
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/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
@ -8,8 +10,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
@ -78,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.8.2 h1:A1iVoZJUex7buGv1CpnC5uwNuyTMBYpDAmBnAQmia9Q=
github.com/compose-spec/compose-go/v2 v2.8.2/go.mod h1:Oky9AZGTRB4E+0VbTPZTUu4Kp+oEMMuwZXZtPPVT1iE=
github.com/compose-spec/compose-go/v2 v2.6.4 h1:Gjv6x8eAhqwwWvoXIo0oZ4bDQBh0OMwdU7LUL9PDLiM=
github.com/compose-spec/compose-go/v2 v2.6.4/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/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.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.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ=
github.com/containerd/containerd/v2 v2.1.4/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
github.com/containerd/containerd/v2 v2.1.1 h1:znnkm7Ajz8lg8BcIPMhc/9yjBRN3B+OkNKqKisKfwwM=
github.com/containerd/containerd/v2 v2.1.1/go.mod h1:zIfkQj4RIodclYQkX7GSSswSwgP8d/XxDOtOAoSDIGU=
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=
@ -98,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.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg=
github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM=
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/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=
@ -111,9 +113,8 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
@ -124,26 +125,24 @@ 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/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.28.0 h1:ZnrVsZ/qQwSOQ4Fx3IgXjiurAwvocaF1YUaPbIXD89E=
github.com/docker/buildx v0.28.0/go.mod h1:nLwx58w7xrQbLVSXiWiHpkVhY4ou4ci/hYomc139Vjk=
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/buildx v0.24.0 h1:qiD+xktY+Fs3R79oz8M+7pbhip78qGLx6LBuVmyb+64=
github.com/docker/buildx v0.24.0/go.mod h1:vYkdBUBjFo/i5vUE0mkajGlk03gE0T/HaGXXhgIxo8E=
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.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.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=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
@ -170,8 +169,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@ -187,8 +186,8 @@ github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -217,8 +216,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-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
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=
@ -244,8 +243,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/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/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/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=
@ -316,8 +315,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.24.0 h1:qYfTl7W1SIJzWDIDCcPT8FboHIZCYfi++wvySi3eyFE=
github.com/moby/buildkit v0.24.0/go.mod h1:4qovICAdR2H4C7+EGMRva5zgHW1gyhT4/flHI7F5F9k=
github.com/moby/buildkit v0.22.0 h1:aWN06w1YGSVN1XfeZbj2ZbgY+zi5xDAjEFI8Cy9fTjA=
github.com/moby/buildkit v0.22.0/go.mod h1:j4pP5hxiTWcz7xuTK2cyxQislHl/N2WWHzOy43DlLJw=
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=
@ -424,10 +423,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
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/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/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/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=
@ -439,19 +436,18 @@ 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.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
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/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=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c h1:2EejZtjFjKJGk71ANb+wtFK5EjUzUkEM3R0xnp559xg=
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -468,18 +464,18 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
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-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/fsutil v0.0.0-20250417144416-3f76f8130144 h1:k9tdF32oJYwtjzMx+D26M6eYiCaAPdJ7tyN7tF1oU5Q=
github.com/tonistiigi/fsutil v0.0.0-20250417144416-3f76f8130144/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/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=
@ -488,6 +484,13 @@ github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnn
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=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -501,38 +504,36 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
go.opentelemetry.io/contrib/instrumentation/net/http/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.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.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
go.opentelemetry.io/otel 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.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.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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=
@ -541,13 +542,13 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
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/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -557,10 +558,10 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/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=
@ -568,8 +569,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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.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=
@ -591,19 +592,19 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -611,19 +612,19 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=

View File

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

View File

@ -105,9 +105,7 @@ func PushManifest(
) error {
// Check if we need an extra empty layer for the manifest config
if ociVersion == api.OCIVersion1_1 || ociVersion == "" {
if err := resolver.Push(ctx, named, v1.DescriptorEmptyJSON, v1.DescriptorEmptyJSON.Data); err != nil {
return err
}
layers = append(layers, Pushable{Descriptor: v1.DescriptorEmptyJSON, Data: []byte("{}")})
}
// prepare to push the manifest by pushing the layers
layerDescriptors := make([]v1.Descriptor, len(layers))

View File

@ -1,38 +0,0 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import "github.com/distribution/reference"
const (
// IndexHostname is the index hostname, used for authentication and image search.
IndexHostname = "index.docker.io"
// IndexServer is used for user auth and image search
IndexServer = "https://index.docker.io/v1/"
// IndexName is the name of the index
IndexName = "docker.io"
)
// GetAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
func GetAuthConfigKey(reposName reference.Named) string {
indexName := reference.Domain(reposName)
if indexName == IndexName || indexName == IndexHostname {
return IndexServer
}
return indexName
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,35 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"testing"
"github.com/docker/compose/v2/internal"
"github.com/hashicorp/go-version"
"gotest.tools/v3/assert"
)
func TestComposeVersionInitialization(t *testing.T) {
v, err := version.NewVersion(internal.Version)
if err != nil {
assert.Equal(t, "", ComposeVersion, "ComposeVersion should be empty for a non-semver internal version (e.g. 'devel')")
} else {
expected := v.Core().String()
assert.Equal(t, expected, ComposeVersion, "ComposeVersion should be the core of internal.Version")
}
}

View File

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

View File

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

View File

@ -1,91 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"errors"
"fmt"
"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
}
if s.dockerCli.ServerInfo().OSType == "windows" {
return nil, errors.New("use_api_socket can't be used with a Windows Docker Engine")
}
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 {
if !service.UseAPISocket {
continue
}
service.Volumes = append(service.Volumes, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind,
Source: "/var/run/docker.sock",
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

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

View File

@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
@ -29,6 +28,7 @@ import (
"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"
@ -79,19 +79,16 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
policy = types.IncludeDependencies
}
if len(options.Services) == 0 {
options.Services = project.ServiceNames()
}
// also include services used as additional_contexts with service: prefix
options.Services = addBuildDependencies(options.Services, project)
// Some build dependencies we just introduced may not be enabled
var err error
project, err = project.WithServicesEnabled(options.Services...)
if err != nil {
return nil, err
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
@ -176,7 +173,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
if options.Quiet {
options.Progress = progress.ModeQuiet
}
if options.Progress == progress.ModeAuto {
if options.Progress == "" {
options.Progress = os.Getenv("BUILDKIT_PROGRESS")
}
w, err = xprogress.NewPrinter(progressCtx, os.Stdout, progressui.DisplayMode(options.Progress),
@ -401,7 +398,6 @@ func resolveAndMergeBuildArgs(dockerCli command.Cli, project *types.Project, ser
return result
}
//nolint:gocyclo
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
plats, err := parsePlatforms(service)
if err != nil {
@ -476,19 +472,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
}
attests := map[string]*string{}
if options.Attestations {
if service.Build.Provenance != "" {
attests["provenance"] = attestation(service.Build.Provenance, "provenance")
}
if service.Build.SBOM != "" {
attests["sbom"] = attestation(service.Build.SBOM, "sbom")
}
}
if options.Provenance != "" {
attests["provenance"] = attestation(options.Provenance, "provenance")
}
if options.SBOM != "" {
attests["sbom"] = attestation(options.SBOM, "sbom")
if !options.Provenance {
attests["provenance"] = nil
}
return build.Options{
@ -498,8 +483,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
NamedContexts: toBuildContexts(service, project),
},
CacheFrom: build.CreateCaches(cacheFrom),
CacheTo: build.CreateCaches(cacheTo),
CacheFrom: pb.CreateCaches(cacheFrom.ToPB()),
CacheTo: pb.CreateCaches(cacheTo.ToPB()),
NoCache: service.Build.NoCache,
Pull: service.Build.Pull,
BuildArgs: flatten(resolveAndMergeBuildArgs(s.dockerCli, project, service, options)),
@ -518,16 +503,6 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
}, nil
}
func attestation(attest string, val string) *string {
if b, err := strconv.ParseBool(val); err == nil {
s := fmt.Sprintf("type=%s,disabled=%t", attest, b)
return &s
} else {
s := fmt.Sprintf("type=%s,%s", attest, val)
return &s
}
}
func toUlimitOpt(ulimits map[string]*types.UlimitsConfig) *cliopts.UlimitOpt {
ref := map[string]*container.Ulimit{}
for _, limit := range toUlimits(ulimits) {
@ -653,11 +628,7 @@ func parsePlatforms(service types.ServiceConfig) ([]specs.Platform, error) {
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
b := project.Services[service].Build
if b != nil {
for _, target := range b.AdditionalContexts {
if s, found := strings.CutPrefix(target, types.ServicePrefix); found {

View File

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

View File

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

View File

@ -22,6 +22,23 @@ import (
moby "github.com/docker/docker/api/types"
)
const (
// ContainerCreated created status
ContainerCreated = "created"
// ContainerRestarting restarting status
ContainerRestarting = "restarting"
// ContainerRunning running status
ContainerRunning = "running"
// ContainerRemoving removing status
ContainerRemoving = "removing"
// ContainerPaused paused status
ContainerPaused = "paused"
// ContainerExited exited status
ContainerExited = "exited"
// ContainerDead dead status
ContainerDead = "dead"
)
var _ io.ReadCloser = ContainerStdout{}
// ContainerStdout implement ReadCloser for moby.HijackedResponse

View File

@ -100,7 +100,7 @@ func (s *composeService) getSpecifiedContainer(ctx context.Context, projectName
IsOneOffLabelTrueX := containers[i].Labels[api.OneoffLabel] == "True"
IsOneOffLabelTrueY := containers[j].Labels[api.OneoffLabel] == "True"
if IsOneOffLabelTrueX || IsOneOffLabelTrueY {
if numberLabelX == numberLabelY {
return !IsOneOffLabelTrueX && IsOneOffLabelTrueY
}
@ -128,6 +128,12 @@ func isService(services ...string) containerPredicate {
}
}
func isRunning() containerPredicate {
return func(c container.Summary) bool {
return c.State == "running"
}
}
// isOrphaned is a predicate to select containers without a matching service definition in compose project
func isOrphaned(project *types.Project) containerPredicate {
services := append(project.ServiceNames(), project.DisabledServiceNames()...)
@ -135,7 +141,7 @@ func isOrphaned(project *types.Project) containerPredicate {
// One-off container
v, ok := c.Labels[api.OneoffLabel]
if ok && v == "True" {
return c.State == container.StateExited || c.State == container.StateDead
return c.State == ContainerExited || c.State == ContainerDead
}
// Service that is not defined in the compose model
service := c.Labels[api.ServiceLabel]

View File

@ -20,7 +20,6 @@ import (
"context"
"errors"
"fmt"
"maps"
"slices"
"sort"
"strconv"
@ -30,7 +29,7 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/docker/docker/api/types/container"
containerType "github.com/docker/docker/api/types/container"
mmount "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/versions"
specs "github.com/opencontainers/image-spec/specs-go/v1"
@ -152,19 +151,19 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
})
slices.Reverse(containers)
for i, ctr := range containers {
for i, container := range containers {
if i >= expected {
// Scale Down
// As we sorted containers, obsolete ones and/or highest number will be removed
ctr := ctr
traceOpts := append(tracing.ServiceOptions(service), tracing.ContainerOptions(ctr)...)
container := container
traceOpts := append(tracing.ServiceOptions(service), tracing.ContainerOptions(container)...)
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "service/scale/down", traceOpts, func(ctx context.Context) error {
return c.service.stopAndRemoveContainer(ctx, ctr, &service, timeout, false)
return c.service.stopAndRemoveContainer(ctx, container, &service, timeout, false)
}))
continue
}
mustRecreate, err := c.mustRecreate(service, ctr, recreate)
mustRecreate, err := c.mustRecreate(service, container, recreate)
if err != nil {
return err
}
@ -174,9 +173,9 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
return err
}
i, ctr := i, ctr
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "container/recreate", tracing.ContainerOptions(ctr), func(ctx context.Context) error {
recreated, err := c.service.recreateContainer(ctx, project, service, ctr, inherit, timeout)
i, container := i, container
eg.Go(tracing.SpanWrapFuncForErrGroup(ctx, "container/recreate", tracing.ContainerOptions(container), func(ctx context.Context) error {
recreated, err := c.service.recreateContainer(ctx, project, service, container, inherit, timeout)
updated[i] = recreated
return err
}))
@ -185,20 +184,20 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
// Enforce non-diverged containers are running
w := progress.ContextWriter(ctx)
name := getContainerProgressName(ctr)
switch ctr.State {
case container.StateRunning:
name := getContainerProgressName(container)
switch container.State {
case ContainerRunning:
w.Event(progress.RunningEvent(name))
case container.StateCreated:
case container.StateRestarting:
case container.StateExited:
case ContainerCreated:
case ContainerRestarting:
case ContainerExited:
default:
ctr := ctr
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/start", tracing.ContainerOptions(ctr), func(ctx context.Context) error {
return c.service.startContainer(ctx, ctr)
container := container
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/start", tracing.ContainerOptions(container), func(ctx context.Context) error {
return c.service.startContainer(ctx, container)
}))
}
updated[i] = ctr
updated[i] = container
}
next := nextContainerNumber(containers)
@ -214,8 +213,8 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels),
}
ctr, err := c.service.createContainer(ctx, project, service, name, number, opts)
updated[actual+i] = ctr
container, err := c.service.createContainer(ctx, project, service, name, number, opts)
updated[actual+i] = container
return err
}))
continue
@ -237,7 +236,7 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
err := c.service.stop(ctx, project.Name, api.StopOptions{
Services: dependents,
Project: project,
}, nil)
})
if err != nil {
return err
}
@ -245,7 +244,7 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
for _, name := range dependents {
dependentStates := c.getObservedState(name)
for i, dependent := range dependentStates {
dependent.State = container.StateExited
dependent.State = ContainerExited
dependentStates[i] = dependent
}
c.setObservedState(name, dependentStates)
@ -328,7 +327,7 @@ func (c *convergence) resolveSharedNamespaces(service *types.ServiceConfig) erro
return nil
}
func (c *convergence) mustRecreate(expected types.ServiceConfig, actual container.Summary, policy string) (bool, error) {
func (c *convergence) mustRecreate(expected types.ServiceConfig, actual containerType.Summary, policy string) (bool, error) {
if policy == api.RecreateNever {
return false, nil
}
@ -360,7 +359,7 @@ func (c *convergence) mustRecreate(expected types.ServiceConfig, actual containe
return false, nil
}
func checkExpectedNetworks(expected types.ServiceConfig, actual container.Summary, networks map[string]string) bool {
func checkExpectedNetworks(expected types.ServiceConfig, actual containerType.Summary, networks map[string]string) bool {
// check the networks container is connected to are the expected ones
for net := range expected.Networks {
id := networks[net]
@ -383,7 +382,7 @@ func checkExpectedNetworks(expected types.ServiceConfig, actual container.Summar
return false
}
func checkExpectedVolumes(expected types.ServiceConfig, actual container.Summary, volumes map[string]string) bool {
func checkExpectedVolumes(expected types.ServiceConfig, actual containerType.Summary, volumes map[string]string) bool {
// check container's volume mounts and search for the expected ones
for _, vol := range expected.Volumes {
if vol.Type != string(mmount.TypeVolume) {
@ -423,7 +422,7 @@ func getDefaultContainerName(projectName, serviceName, index string) string {
return strings.Join([]string{projectName, serviceName, index}, api.Separator)
}
func getContainerProgressName(ctr container.Summary) string {
func getContainerProgressName(ctr containerType.Summary) string {
return "Container " + getCanonicalContainerName(ctr)
}
@ -564,14 +563,11 @@ func shouldWaitForDependency(serviceName string, dependencyConfig types.ServiceD
} else if service.GetScale() == 0 {
// don't wait for the dependency which configured to have 0 containers running
return false, nil
} else if service.Provider != nil {
// don't wait for provider services
return false, nil
}
return true, nil
}
func nextContainerNumber(containers []container.Summary) int {
func nextContainerNumber(containers []containerType.Summary) int {
maxNumber := 0
for _, c := range containers {
s, ok := c.Labels[api.ContainerNumberLabel]
@ -592,7 +588,7 @@ func nextContainerNumber(containers []container.Summary) int {
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
name string, number int, opts createOptions,
) (ctr container.Summary, err error) {
) (ctr containerType.Summary, err error) {
w := progress.ContextWriter(ctx)
eventName := "Container " + name
w.Event(progress.CreatingEvent(eventName))
@ -612,8 +608,8 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro
}
func (s *composeService) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
replaced container.Summary, inherit bool, timeout *time.Duration,
) (created container.Summary, err error) {
replaced containerType.Summary, inherit bool, timeout *time.Duration,
) (created containerType.Summary, err error) {
w := progress.ContextWriter(ctx)
eventName := getContainerProgressName(replaced)
w.Event(progress.NewEvent(eventName, progress.Working, "Recreate"))
@ -632,22 +628,17 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
return created, err
}
var inherited *container.Summary
var inherited *containerType.Summary
if inherit {
inherited = &replaced
}
replacedContainerName := service.ContainerName
if replacedContainerName == "" {
replacedContainerName = service.Name + api.Separator + strconv.Itoa(number)
}
name := getContainerName(project.Name, service, number)
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
opts := createOptions{
AutoRemove: false,
AttachStdin: false,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replacedContainerName),
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID),
}
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts, w)
if err != nil {
@ -655,17 +646,17 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
}
timeoutInSecond := utils.DurationSecondToInt(timeout)
err = s.apiClient().ContainerStop(ctx, replaced.ID, container.StopOptions{Timeout: timeoutInSecond})
err = s.apiClient().ContainerStop(ctx, replaced.ID, containerType.StopOptions{Timeout: timeoutInSecond})
if err != nil {
return created, err
}
err = s.apiClient().ContainerRemove(ctx, replaced.ID, container.RemoveOptions{})
err = s.apiClient().ContainerRemove(ctx, replaced.ID, containerType.RemoveOptions{})
if err != nil {
return created, err
}
err = s.apiClient().ContainerRename(ctx, tmpName, name)
err = s.apiClient().ContainerRename(ctx, created.ID, name)
if err != nil {
return created, err
}
@ -677,12 +668,12 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
// 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 container.Summary) error {
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, container.StartOptions{})
err := s.apiClient().ContainerStart(ctx, ctr.ID, containerType.StartOptions{})
if err != nil {
return err
}
@ -695,11 +686,11 @@ func (s *composeService) createMobyContainer(ctx context.Context,
service types.ServiceConfig,
name string,
number int,
inherit *container.Summary,
inherit *containerType.Summary,
opts createOptions,
w progress.Writer,
) (container.Summary, error) {
var created container.Summary
) (containerType.Summary, error) {
var created containerType.Summary
cfgs, err := s.getCreateConfigs(ctx, project, service, number, inherit, opts)
if err != nil {
return created, err
@ -733,11 +724,11 @@ func (s *composeService) createMobyContainer(ctx context.Context,
if err != nil {
return created, err
}
created = container.Summary{
created = containerType.Summary{
ID: inspectedContainer.ID,
Labels: inspectedContainer.Config.Labels,
Names: []string{inspectedContainer.Name},
NetworkSettings: &container.NetworkSettingsSummary{
NetworkSettings: &containerType.NetworkSettingsSummary{
Networks: inspectedContainer.NetworkSettings.Networks,
},
}
@ -765,7 +756,14 @@ func (s *composeService) createMobyContainer(ctx context.Context,
}
}
}
return created, nil
err = s.injectSecrets(ctx, project, service, created.ID)
if err != nil {
return created, err
}
err = s.injectConfigs(ctx, project, service, created.ID)
return created, err
}
// getLinks mimics V1 compose/service.py::Service::_get_links()
@ -828,33 +826,33 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) {
for _, c := range containers {
ctr, err := s.apiClient().ContainerInspect(ctx, c.ID)
container, err := s.apiClient().ContainerInspect(ctx, c.ID)
if err != nil {
return false, err
}
name := ctr.Name[1:]
name := container.Name[1:]
if ctr.State.Status == container.StateExited {
return false, fmt.Errorf("container %s exited (%d)", name, ctr.State.ExitCode)
if container.State.Status == "exited" {
return false, fmt.Errorf("container %s exited (%d)", name, container.State.ExitCode)
}
if ctr.Config.Healthcheck == nil && fallbackRunning {
if container.Config.Healthcheck == nil && fallbackRunning {
// Container does not define a health check, but we can fall back to "running" state
return ctr.State != nil && ctr.State.Status == container.StateRunning, nil
return container.State != nil && container.State.Status == "running", nil
}
if ctr.State == nil || ctr.State.Health == nil {
if container.State == nil || container.State.Health == nil {
return false, fmt.Errorf("container %s has no healthcheck configured", name)
}
switch ctr.State.Health.Status {
case container.Healthy:
switch container.State.Health.Status {
case containerType.Healthy:
// Continue by checking the next container.
case container.Unhealthy:
case containerType.Unhealthy:
return false, fmt.Errorf("container %s is unhealthy", name)
case container.Starting:
case containerType.Starting:
return false, nil
default:
return false, fmt.Errorf("container %s had unexpected health status %q", name, ctr.State.Health.Status)
return false, fmt.Errorf("container %s had unexpected health status %q", name, container.State.Health.Status)
}
}
return true, nil
@ -862,12 +860,12 @@ func (s *composeService) isServiceHealthy(ctx context.Context, containers Contai
func (s *composeService) isServiceCompleted(ctx context.Context, containers Containers) (bool, int, error) {
for _, c := range containers {
ctr, err := s.apiClient().ContainerInspect(ctx, c.ID)
container, err := s.apiClient().ContainerInspect(ctx, c.ID)
if err != nil {
return false, 0, err
}
if ctr.State != nil && ctr.State.Status == container.StateExited {
return true, ctr.State.ExitCode, nil
if container.State != nil && container.State.Status == "exited" {
return true, container.State.ExitCode, nil
}
}
return false, 0, nil
@ -896,23 +894,12 @@ func (s *composeService) startService(ctx context.Context,
w := progress.ContextWriter(ctx)
for _, ctr := range containers.filter(isService(service.Name)) {
if ctr.State == container.StateRunning {
if ctr.State == ContainerRunning {
continue
}
err = s.injectSecrets(ctx, project, service, ctr.ID)
if err != nil {
return err
}
err = s.injectConfigs(ctx, project, service, ctr.ID)
if err != nil {
return err
}
eventName := getContainerProgressName(ctr)
w.Event(progress.StartingEvent(eventName))
err = s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{})
err = s.apiClient().ContainerStart(ctx, ctr.ID, containerType.StartOptions{})
if err != nil {
return err
}
@ -932,7 +919,9 @@ func (s *composeService) startService(ctx context.Context,
func mergeLabels(ls ...types.Labels) types.Labels {
merged := types.Labels{}
for _, l := range ls {
maps.Copy(merged, l)
for k, v := range l {
merged[k] = v
}
}
return merged
}

View File

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

View File

@ -30,12 +30,13 @@ import (
"github.com/compose-spec/compose-go/v2/paths"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
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"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/versions"
volumetypes "github.com/docker/docker/api/types/volume"
"github.com/docker/go-connections/nat"
@ -82,11 +83,6 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
return err
}
err = s.ensureModels(ctx, project, options.QuietPull)
if err != nil {
return err
}
prepareNetworks(project)
networks, err := s.ensureNetworks(ctx, project)
@ -118,13 +114,6 @@ 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)
}
@ -180,12 +169,15 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
return createConfigs{}, err
}
var runCmd, entrypoint []string
var (
runCmd strslice.StrSlice
entrypoint strslice.StrSlice
)
if service.Command != nil {
runCmd = service.Command
runCmd = strslice.StrSlice(service.Command)
}
if service.Entrypoint != nil {
entrypoint = service.Entrypoint
entrypoint = strslice.StrSlice(service.Entrypoint)
}
var (
@ -282,8 +274,8 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
Annotations: service.Annotations,
Binds: binds,
Mounts: mounts,
CapAdd: service.CapAdd,
CapDrop: service.CapDrop,
CapAdd: strslice.StrSlice(service.CapAdd),
CapDrop: strslice.StrSlice(service.CapDrop),
NetworkMode: networkMode,
Init: service.Init,
IpcMode: container.IpcMode(service.Ipc),
@ -1262,7 +1254,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)
@ -1428,7 +1420,7 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
err := s.stop(ctx, project.Name, api.StopOptions{
Services: services,
Project: project,
}, nil)
})
if err != nil {
return nil, err
}
@ -1497,7 +1489,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
sn, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
if err == nil {
networks = append(networks, sn)
} else if !errdefs.IsNotFound(err) {
} else if !cerrdefs.IsNotFound(err) {
return "", err
}
@ -1534,7 +1526,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 {
@ -1599,7 +1591,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
err := s.stop(ctx, project.Name, api.StopOptions{
Services: services,
Project: project,
}, nil)
})
if err != nil {
return err
}
@ -1623,7 +1615,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
}
func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {
eventName := fmt.Sprintf("Volume %s", volume.Name)
eventName := fmt.Sprintf("Volume %q", volume.Name)
w := progress.ContextWriter(ctx)
w.Event(progress.CreatingEvent(eventName))
hash, err := VolumeHash(volume)

View File

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

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ import (
"sync"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
cerrdefs "github.com/containerd/errdefs"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
@ -204,7 +204,7 @@ func (p *ImagePruner) filterImagesByExistence(ctx context.Context, imageNames []
for _, img := range imageNames {
eg.Go(func() error {
_, err := p.client.ImageInspect(ctx, img)
if 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

@ -22,9 +22,8 @@ import (
"slices"
"strings"
"sync"
"time"
"github.com/containerd/errdefs"
cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/container"
@ -91,11 +90,6 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
}
}
created, err := time.Parse(time.RFC3339Nano, image.Created)
if err != nil {
return err
}
mux.Lock()
defer mux.Unlock()
summary[getCanonicalContainerName(c)] = api.ImageSummary{
@ -109,7 +103,6 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
Variant: image.Variant,
},
Size: image.Size,
Created: created,
LastTagTime: image.Metadata.LastTagTime,
}
return nil
@ -128,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

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

View File

@ -111,7 +111,7 @@ func testContainer(service string, id string, oneOff bool) container.Summary {
ID: id,
Names: []string{name},
Labels: containerLabels(service, oneOff),
State: container.StateExited,
State: ContainerExited,
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -27,15 +27,16 @@ import (
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/config"
"github.com/docker/compose/v2/pkg/progress"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
type JsonMessage struct {
@ -51,8 +52,6 @@ const (
providerMetadataDirectory = "compose/providers"
)
var mux sync.Mutex
func (s *composeService) runPlugin(ctx context.Context, project *types.Project, service types.ServiceConfig, command string) error {
provider := *service.Provider
@ -71,8 +70,6 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
return err
}
mux.Lock()
defer mux.Unlock()
for name, s := range project.Services {
if _, ok := s.DependsOn[service.Name]; ok {
prefix := strings.ToUpper(service.Name) + "_"
@ -164,14 +161,14 @@ func (s *composeService) getPluginBinaryPath(provider string) (path string, err
if err == nil {
path = plugin.Path
}
if errdefs.IsNotFound(err) {
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, project)
cmdOptionsMetadata := s.getPluginMetadata(path, service.Provider.Type)
var currentCommandMetadata CommandMetadata
switch command {
case "up":
@ -179,9 +176,8 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
case "down":
currentCommandMetadata = cmdOptionsMetadata.Down
}
commandMetadataIsEmpty := len(currentCommandMetadata.Parameters) == 0
provider := *service.Provider
commandMetadataIsEmpty := cmdOptionsMetadata.IsEmpty()
if err := currentCommandMetadata.CheckRequiredParameters(provider); !commandMetadataIsEmpty && err != nil {
return nil, err
}
@ -197,21 +193,35 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
args = append(args, service.Name)
cmd := exec.CommandContext(ctx, path, args...)
err := s.prepareShellOut(ctx, project.Environment, cmd)
if err != nil {
return nil, err
// 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())
}
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, &carrier)
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
return cmd, nil
}
func (s *composeService) getPluginMetadata(path, command string, project *types.Project) ProviderMetadata {
func (s *composeService) getPluginMetadata(path, command string) ProviderMetadata {
cmd := exec.Command(path, "compose", "metadata")
err := s.prepareShellOut(context.Background(), project.Environment, cmd)
if err != nil {
logrus.Debugf("failed to prepare plugin metadata command: %v", err)
return ProviderMetadata{}
}
stdout := &bytes.Buffer{}
cmd.Stdout = stdout
@ -246,10 +256,6 @@ type ProviderMetadata struct {
Down CommandMetadata `json:"down"`
}
func (p ProviderMetadata) IsEmpty() bool {
return p.Description == "" && p.Up.Parameters == nil && p.Down.Parameters == nil
}
type CommandMetadata struct {
Parameters []ParameterMetadata `json:"parameters"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,6 @@ import (
"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/progress"
"github.com/docker/docker/pkg/stringid"
)
@ -59,19 +58,6 @@ 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
@ -123,23 +109,11 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
return "", err
}
err = s.ensureModels(ctx, project, opts.QuietPull)
if err != nil {
return "", err
}
created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
if err != nil {
return "", err
}
err = s.injectSecrets(ctx, project, service, created.ID)
if err != nil {
return created.ID, err
}
err = s.injectConfigs(ctx, project, service, created.ID)
return created.ID, err
return created.ID, nil
}
func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
@ -186,24 +160,3 @@ 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 {
project = project.WithServicesDisabled(options.Service)
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(project.Services) > 0 {
return s.Start(ctx, project.Name, api.StartOptions{
Project: project,
})
}
return nil
}

View File

@ -1,86 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"os"
"os/exec"
"path/filepath"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v2/internal"
"github.com/docker/docker/client"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
// prepareShellOut prepare a shell-out command to be ran by Compose
func (s *composeService) prepareShellOut(gctx context.Context, env types.Mapping, cmd *exec.Cmd) error {
env = env.Clone()
// remove DOCKER_CLI_PLUGIN... variable so a docker-cli plugin will detect it run standalone
delete(env, metadata.ReexecEnvvar)
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(gctx, &carrier)
env.Merge(types.Mapping(carrier))
cmd.Env = env.Values()
return nil
}
// propagateDockerEndpoint produces DOCKER_* env vars for a child CLI plugin to target the same docker endpoint
// `cleanup` func MUST be called after child process completion to enforce removal of cert files
func (s *composeService) propagateDockerEndpoint() ([]string, func(), error) {
cleanup := func() {}
env := types.Mapping{}
env[command.EnvOverrideContext] = s.dockerCli.CurrentContext()
env["USER_AGENT"] = "compose/" + internal.Version
endpoint := s.dockerCli.DockerEndpoint()
env[client.EnvOverrideHost] = endpoint.Host
if endpoint.TLSData != nil {
certs, err := os.MkdirTemp("", "compose")
if err != nil {
return nil, cleanup, err
}
cleanup = func() {
_ = os.RemoveAll(certs)
}
env[client.EnvOverrideCertPath] = certs
if !endpoint.SkipTLSVerify {
env[client.EnvTLSVerify] = "1"
}
err = os.WriteFile(filepath.Join(certs, flags.DefaultKeyFile), endpoint.TLSData.Key, 0o600)
if err != nil {
return nil, cleanup, err
}
err = os.WriteFile(filepath.Join(certs, flags.DefaultCaFile), endpoint.TLSData.Cert, 0o600)
if err != nil {
return nil, cleanup, err
}
err = os.WriteFile(filepath.Join(certs, flags.DefaultCaFile), endpoint.TLSData.CA, 0o600)
if err != nil {
return nil, cleanup, err
}
}
return env.Values(), cleanup, nil
}

View File

@ -20,14 +20,19 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"time"
cerrdefs "github.com/containerd/errdefs"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils"
containerType "github.com/docker/docker/api/types/container"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/docker/api/types/filters"
"golang.org/x/sync/errgroup"
)
func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
@ -51,6 +56,60 @@ func (s *composeService) start(ctx context.Context, projectName string, options
}
}
// use an independent context tied to the errgroup for background attach operations
// the primary context is still used for other operations
// this means that once any attach operation fails, all other attaches are cancelled,
// but an attach failing won't interfere with the rest of the start
eg, attachCtx := errgroup.WithContext(ctx)
if listener != nil {
_, err := s.attach(attachCtx, project, listener, options.AttachTo)
if err != nil {
return err
}
eg.Go(func() error {
// it's possible to have a required service whose log output is not desired
// (i.e. it's not in the attach set), so watch everything and then filter
// calls to attach; this ensures that `watchContainers` blocks until all
// required containers have exited, even if their output is not being shown
attachTo := utils.NewSet[string](options.AttachTo...)
required := utils.NewSet[string](options.Services...)
toWatch := attachTo.Union(required).Elements()
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true, toWatch...)
if err != nil {
return err
}
// N.B. this uses the parent context (instead of attachCtx) so that the watch itself can
// continue even if one of the log streams fails
return s.watchContainers(ctx, project.Name, toWatch, required.Elements(), listener, containers,
func(ctr containerType.Summary, _ time.Time) error {
svc := ctr.Labels[api.ServiceLabel]
if attachTo.Has(svc) {
return s.attachContainer(attachCtx, ctr, listener)
}
// HACK: simulate an "attach" event
listener(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: getContainerNameWithoutProject(ctr),
ID: ctr.ID,
Service: svc,
})
return nil
}, func(ctr containerType.Summary, _ time.Time) error {
listener(api.ContainerEvent{
Type: api.ContainerEventAttach,
Container: "", // actual name will be set by start event
ID: ctr.ID,
Service: ctr.Labels[api.ServiceLabel],
})
return nil
})
})
}
var containers Containers
containers, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
Filters: filters.NewArgs(
@ -98,7 +157,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
}
}
return nil
return eg.Wait()
}
// getDependencyCondition checks if service is depended on by other services
@ -114,3 +173,182 @@ func getDependencyCondition(service types.ServiceConfig, project *types.Project)
}
return ServiceConditionRunningOrHealthy
}
type containerWatchFn func(ctr containerType.Summary, t time.Time) error
// watchContainers uses engine events to capture container start/die and notify ContainerEventListener
func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
projectName string, services, required []string,
listener api.ContainerEventListener, containers Containers, onStart, onRecreate containerWatchFn,
) error {
if len(containers) == 0 {
return nil
}
if len(required) == 0 {
required = services
}
unexpected := utils.NewSet[string](required...).Diff(utils.NewSet[string](services...))
if len(unexpected) != 0 {
return fmt.Errorf(`required service(s) "%s" not present in watched service(s) "%s"`,
strings.Join(unexpected.Elements(), ", "),
strings.Join(services, ", "))
}
// predicate to tell if a container we receive event for should be considered or ignored
ofInterest := func(c containerType.Summary) bool {
if len(services) > 0 {
// we only watch some services
return slices.Contains(services, c.Labels[api.ServiceLabel])
}
return true
}
// predicate to tell if a container we receive event for should be watched until termination
isRequired := func(c containerType.Summary) bool {
if len(services) > 0 && len(required) > 0 {
// we only watch some services
return slices.Contains(required, c.Labels[api.ServiceLabel])
}
return true
}
var (
expected = utils.NewSet[string]()
watched = map[string]int{}
replaced []string
)
for _, c := range containers {
if isRequired(c) {
expected.Add(c.ID)
}
watched[c.ID] = 0
}
ctx, stop := context.WithCancel(ctx)
err := s.Events(ctx, projectName, api.EventsOptions{
Services: services,
Consumer: func(event api.Event) error {
defer func() {
// after consuming each event, check to see if we're done
if len(expected) == 0 {
stop()
}
}()
inspected, err := s.apiClient().ContainerInspect(ctx, event.Container)
if err != nil {
if cerrdefs.IsNotFound(err) {
// it's possible to get "destroy" or "kill" events but not
// be able to inspect in time before they're gone from the
// API, so just remove the watch without erroring
delete(watched, event.Container)
expected.Remove(event.Container)
return nil
}
return err
}
container := containerType.Summary{
ID: inspected.ID,
Names: []string{inspected.Name},
Labels: inspected.Config.Labels,
}
name := getContainerNameWithoutProject(container)
service := container.Labels[api.ServiceLabel]
switch event.Status {
case "stop":
if inspected.State.Running {
// on sync+restart action the container stops -> dies -> start -> restart
// we do not want to stop the current container, we want to restart it
return nil
}
if _, ok := watched[container.ID]; ok {
eType := api.ContainerEventStopped
if slices.Contains(replaced, container.ID) {
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
eType = api.ContainerEventRecreated
}
listener(api.ContainerEvent{
Type: eType,
Container: name,
ID: container.ID,
Service: service,
ExitCode: inspected.State.ExitCode,
})
}
delete(watched, container.ID)
expected.Remove(container.ID)
case "die":
restarted := watched[container.ID]
watched[container.ID] = restarted + 1
// Container terminated.
willRestart := inspected.State.Restarting
if inspected.State.Running {
// on sync+restart action inspected.State.Restarting is false,
// however the container is already running before it restarts
willRestart = true
}
eType := api.ContainerEventExit
if slices.Contains(replaced, container.ID) {
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
eType = api.ContainerEventRecreated
}
listener(api.ContainerEvent{
Type: eType,
Container: name,
ID: container.ID,
Service: service,
ExitCode: inspected.State.ExitCode,
Restarting: willRestart,
})
if !willRestart {
// we're done with this one
delete(watched, container.ID)
expected.Remove(container.ID)
}
case "start":
count, ok := watched[container.ID]
mustAttach := ok && count > 0 // Container restarted, need to re-attach
if !ok {
// A new container has just been added to service by scale
watched[container.ID] = 0
expected.Add(container.ID)
mustAttach = true
}
if mustAttach {
// Container restarted, need to re-attach
err := onStart(container, event.Timestamp)
if err != nil {
return err
}
}
case "create":
if id, ok := container.Labels[api.ContainerReplaceLabel]; ok {
replaced = append(replaced, id)
err = onRecreate(container, event.Timestamp)
if err != nil {
return err
}
if expected.Has(id) {
expected.Add(inspected.ID)
expected.Add(container.ID)
}
watched[container.ID] = 1
} else if ofInterest(container) {
watched[container.ID] = 1
if isRequired(container) {
expected.Add(container.ID)
}
}
}
return nil
},
})
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return err
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ import (
)
func TestLocalComposeBuild(t *testing.T) {
for _, env := range []string{"DOCKER_BUILDKIT=0", "DOCKER_BUILDKIT=1,COMPOSE_BAKE=0", "DOCKER_BUILDKIT=1,COMPOSE_BAKE=1"} {
for _, env := range []string{"DOCKER_BUILDKIT=0", "DOCKER_BUILDKIT=1", "DOCKER_BUILDKIT=1,COMPOSE-BAKE=1"} {
c := NewCLI(t, WithEnv(strings.Split(env, ",")...))
t.Run(env+" build named and unnamed images", func(t *testing.T) {
@ -536,75 +536,3 @@ func TestBuildDependsOn(t *testing.T) {
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"))
}
func TestBuildLongOutputLine(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "build", "long-line")
out := res.Combined()
assert.Check(t, strings.Contains(out, "long-line Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "up", "--build", "long-line")
out = res.Combined()
assert.Check(t, strings.Contains(out, "long-line Built"))
}
func TestBuildDependentImageWithProfile(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "build", "secret-build-test")
out := res.Combined()
assert.Check(t, strings.Contains(out, "secret-build-test Built"))
}

View File

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

View File

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

View File

@ -1,47 +0,0 @@
/*
Copyright 2023 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"testing"
"gotest.tools/v3/icmd"
)
func TestExec(t *testing.T) {
const projectName = "e2e-exec"
c := NewParallelCLI(t)
cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()
c.RunDockerComposeCmd(t, "-f", "./fixtures/exec/compose.yaml", "--project-name", projectName, "run", "-d", "test", "cat")
res := c.RunDockerComposeCmdNoCheck(t, "--project-name", projectName, "exec", "--index=1", "test", "ps")
res.Assert(t, icmd.Expected{Err: "service \"test\" is not running container #1", ExitCode: 1})
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "exec", "test", "ps")
res.Assert(t, icmd.Expected{Out: "cat"}) // one-off container was selected
c.RunDockerComposeCmd(t, "-f", "./fixtures/exec/compose.yaml", "--project-name", projectName, "up", "-d")
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "exec", "test", "ps")
res.Assert(t, icmd.Expected{Out: "tail"}) // service container was selected
}

View File

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

View File

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

View File

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

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