mirror of
https://github.com/docker/compose.git
synced 2025-08-31 22:48:11 +02:00
Compare commits
142 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
04b8ac5fe4 | ||
|
d09948da41 | ||
|
f1efbb8322 | ||
|
1d52012b82 | ||
|
1d69f4a68c | ||
|
6078b4d99d | ||
|
73e593e69a | ||
|
51499f645b | ||
|
5165b0f814 | ||
|
93dd1a4558 | ||
|
ba3f5664c0 | ||
|
c420bc44c4 | ||
|
60681a824c | ||
|
19ad737ee7 | ||
|
d3a260e533 | ||
|
e75329dce2 | ||
|
1dc0be2c30 | ||
|
3bac9ffd08 | ||
|
f266715dd0 | ||
|
c2cb0aef6b | ||
|
fbc62d111e | ||
|
0d40064ce8 | ||
|
91a6eafa1d | ||
|
f36ee00f71 | ||
|
29ede3ba7d | ||
|
bf6d7bf47e | ||
|
fc66da06db | ||
|
909211dd61 | ||
|
0dc9852c67 | ||
|
a478702236 | ||
|
2c12ad19db | ||
|
038ea8441a | ||
|
9e98e6101e | ||
|
52f04229c0 | ||
|
28895d0322 | ||
|
a926f7d717 | ||
|
fe046915eb | ||
|
adbd61e5d6 | ||
|
e37ac04329 | ||
|
cab2c2a44e | ||
|
1946de598d | ||
|
8e29a138aa | ||
|
3c8da0afee | ||
|
1b12c867c5 | ||
|
1a4fc55fd7 | ||
|
efc939dcee | ||
|
d6e9f79ba6 | ||
|
b4c44a431f | ||
|
fb5a8644c3 | ||
|
95660c5e5a | ||
|
f6ddd6ae88 | ||
|
4ae7066955 | ||
|
fd954f266c | ||
|
d62e21025c | ||
|
6a2d16bd10 | ||
|
4d47da6dc2 | ||
|
8f91793fb5 | ||
|
1d2223fb23 | ||
|
d4f6000712 | ||
|
c50d16cd78 | ||
|
3875e13fad | ||
|
c89f30170d | ||
|
41a9b91887 | ||
|
5fc2b2a71c | ||
|
b1cd40c316 | ||
|
362ab0733f | ||
|
f35d2cfb3b | ||
|
17ba6c7188 | ||
|
1c37f1abb6 | ||
|
485b6200ee | ||
|
8c17a35609 | ||
|
6b9667401a | ||
|
9a1e589ce8 | ||
|
5e147e852e | ||
|
29308cb97e | ||
|
0b0242d0ac | ||
|
5a704004d3 | ||
|
cb95910018 | ||
|
f42226e352 | ||
|
0cc3c7a550 | ||
|
f7ee9c8a0c | ||
|
35efa97b7d | ||
|
9e17a091be | ||
|
4bbc6c609f | ||
|
69f1430a49 | ||
|
7cf7c6414f | ||
|
0e0ed91a39 | ||
|
66524e7728 | ||
|
c626befee1 | ||
|
60ee6adcd2 | ||
|
8faf1eb808 | ||
|
3b0601b671 | ||
|
f42374bb18 | ||
|
b046a5ef72 | ||
|
1570c6c076 | ||
|
674e13fb6d | ||
|
6fa173124a | ||
|
2c69fc3d4d | ||
|
317ebcd3b0 | ||
|
5cf1f0e2a4 | ||
|
6198ed5bd2 | ||
|
00ccddbde8 | ||
|
63b441401e | ||
|
3a7982fe45 | ||
|
5430caa172 | ||
|
ee1b1e0a93 | ||
|
26e46d7cc8 | ||
|
a9e76943f6 | ||
|
b6a0df8d3c | ||
|
5a063b7510 | ||
|
ae49bba9be | ||
|
51acc58453 | ||
|
7c999d7f93 | ||
|
ad750d6143 | ||
|
fe382df507 | ||
|
6501d59efc | ||
|
33a782572f | ||
|
65803ea12e | ||
|
f613379373 | ||
|
3553aa26a6 | ||
|
257ea7b75d | ||
|
d219aa66f4 | ||
|
c9ebfad78e | ||
|
8e57362a0f | ||
|
29630f184e | ||
|
6514c680a5 | ||
|
3394bf031b | ||
|
832a08f579 | ||
|
aadce87b16 | ||
|
b3207c455d | ||
|
769b7391ba | ||
|
149b882ebf | ||
|
c97e40e2b8 | ||
|
22e23bd4dc | ||
|
2dde5faeb8 | ||
|
f7825a56bf | ||
|
4cf075ea0a | ||
|
4f491ffa98 | ||
|
ea1c26d22a | ||
|
9a5fa05ad6 | ||
|
276c229458 | ||
|
eef448dc64 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -12,6 +12,12 @@ body:
|
||||
Include both the current behavior (what you are seeing) as well as what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
[Docker Swarm](https://www.mirantis.com/software/swarm/) uses a distinct compose file parser and
|
||||
as such doesn't support some of the recent features of Docker Compose. Please contact Mirantis
|
||||
if you need assistance with compose file support in Docker Swarm.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@ -183,6 +183,11 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up Docker Model
|
||||
run: |
|
||||
sudo apt-get install docker-model-plugin
|
||||
docker model version
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
@ -190,6 +195,9 @@ jobs:
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
- name: Build example provider
|
||||
run: make example-provider
|
||||
|
||||
- name: Build
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
|
@ -30,6 +30,8 @@ linters:
|
||||
deny:
|
||||
- pkg: io/ioutil
|
||||
desc: io/ioutil package has been deprecated
|
||||
- pkg: github.com/docker/docker/errdefs
|
||||
desc: use github.com/containerd/errdefs instead.
|
||||
- pkg: golang.org/x/exp/maps
|
||||
desc: use stdlib maps package
|
||||
- pkg: golang.org/x/exp/slices
|
||||
|
@ -4,12 +4,15 @@
|
||||
* Windows:
|
||||
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/windows-install/)
|
||||
* make
|
||||
* go (see [go.mod](go.mod) for minimum version)
|
||||
* macOS:
|
||||
* [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/)
|
||||
* make
|
||||
* go (see [go.mod](go.mod) for minimum version)
|
||||
* Linux:
|
||||
* [Docker 20.10 or later](https://docs.docker.com/engine/install/)
|
||||
* make
|
||||
* go (see [go.mod](go.mod) for minimum version)
|
||||
|
||||
### Building the CLI
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
ARG GO_VERSION=1.23.8
|
||||
ARG GO_VERSION=1.23.12
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOLANGCI_LINT_VERSION=v2.0.2
|
||||
ARG ADDLICENSE_VERSION=v1.0.0
|
||||
|
6
Makefile
6
Makefile
@ -74,7 +74,7 @@ install: binary
|
||||
install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose
|
||||
|
||||
.PHONY: e2e-compose
|
||||
e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
|
||||
e2e-compose: example-provider ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test
|
||||
go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 ./pkg/e2e
|
||||
|
||||
.PHONY: e2e-compose-standalone
|
||||
@ -87,6 +87,10 @@ build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and r
|
||||
.PHONY: build-and-e2e-compose-standalone
|
||||
build-and-e2e-compose-standalone: build e2e-compose-standalone ## Compile the compose cli-plugin and run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test
|
||||
|
||||
.PHONY: example-provider
|
||||
example-provider: ## build example provider for e2e tests
|
||||
go build -o bin/build/example-provider docs/examples/provider.go
|
||||
|
||||
.PHONY: mocks
|
||||
mocks:
|
||||
mockgen --version >/dev/null 2>&1 || go install go.uber.org/mock/mockgen@v0.4.0
|
||||
|
@ -23,6 +23,12 @@ your application are configured.
|
||||
Once you have a Compose file, you can create and start your application with a
|
||||
single command: `docker compose up`.
|
||||
|
||||
> **Note**: About Docker Swarm
|
||||
> Docker Swarm used to rely on the legacy compose file format but did not adopted the compose specification
|
||||
> so is missing some of the recent enhancements in the compose syntax. After
|
||||
> [acquisition by Mirantis](https://www.mirantis.com/software/swarm/) swarm isn't maintained by Docker Inc, and
|
||||
> as such some Docker Compose features aren't accessible to swarm users.
|
||||
|
||||
# Where to get Docker Compose
|
||||
|
||||
### Windows and macOS
|
||||
|
@ -55,8 +55,10 @@ func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
|
||||
ctx,
|
||||
"cli/"+strings.Join(commandName(cmd), "-"),
|
||||
)
|
||||
cmdSpan.SetAttributes(attribute.StringSlice("cli.flags", getFlags(cmd.Flags())))
|
||||
cmdSpan.SetAttributes(attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()))
|
||||
cmdSpan.SetAttributes(
|
||||
attribute.StringSlice("cli.flags", getFlags(cmd.Flags())),
|
||||
attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()),
|
||||
)
|
||||
|
||||
cmd.SetContext(ctx)
|
||||
wrapRunE(cmd, cmdSpan, tracingShutdown)
|
||||
|
@ -45,7 +45,8 @@ type buildOptions struct {
|
||||
deps bool
|
||||
print bool
|
||||
check bool
|
||||
provenance bool
|
||||
sbom string
|
||||
provenance string
|
||||
}
|
||||
|
||||
func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, error) {
|
||||
@ -84,6 +85,7 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
|
||||
Check: opts.check,
|
||||
SSHs: SSHKeys,
|
||||
Builder: builderName,
|
||||
SBOM: opts.sbom,
|
||||
Provenance: opts.provenance,
|
||||
}, nil
|
||||
}
|
||||
@ -119,12 +121,14 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.push, "push", false, "Push service images")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the build output")
|
||||
flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image")
|
||||
flags.StringArrayVar(&opts.args, "build-arg", []string{}, "Set build-time variables for services")
|
||||
flags.StringVar(&opts.ssh, "ssh", "", "Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent)")
|
||||
flags.StringVar(&opts.builder, "builder", "", "Set builder to use")
|
||||
flags.BoolVar(&opts.deps, "with-dependencies", false, "Also build dependencies (transitively)")
|
||||
flags.StringVar(&opts.provenance, "provenance", "", `Add a provenance attestation`)
|
||||
flags.StringVar(&opts.sbom, "sbom", "", `Add a SBOM attestation`)
|
||||
|
||||
flags.Bool("parallel", true, "Build images in parallel. DEPRECATED")
|
||||
flags.MarkHidden("parallel") //nolint:errcheck
|
||||
@ -145,6 +149,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
}
|
||||
|
||||
func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error {
|
||||
opts.All = true // do not drop resources as build may involve some dependencies by additional_contexts
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -155,7 +160,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o
|
||||
}
|
||||
|
||||
apiBuildOptions, err := opts.toAPIBuildOptions(services)
|
||||
apiBuildOptions.Provenance = true
|
||||
apiBuildOptions.Attestations = true
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ import (
|
||||
composegoutils "github.com/compose-spec/compose-go/v2/utils"
|
||||
"github.com/docker/buildx/util/logutil"
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/pkg/kvfile"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
@ -245,7 +245,7 @@ func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cl
|
||||
name := o.ProjectName
|
||||
var project *types.Project
|
||||
if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
|
||||
p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile)
|
||||
p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
envProjectName := os.Getenv(ComposeProjectName)
|
||||
if envProjectName != "" {
|
||||
@ -416,7 +416,7 @@ const PluginName = "compose"
|
||||
|
||||
// RunningAsStandalone detects when running as a standalone program
|
||||
func RunningAsStandalone() bool {
|
||||
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName && os.Args[1] != PluginName
|
||||
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName && os.Args[1] != PluginName
|
||||
}
|
||||
|
||||
// RootCommand returns the compose command with its child commands
|
||||
@ -636,6 +636,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
publishCommand(&opts, dockerCli, backend),
|
||||
alphaCommand(&opts, dockerCli, backend),
|
||||
bridgeCommand(&opts, dockerCli),
|
||||
volumesCommand(&opts, dockerCli, backend),
|
||||
)
|
||||
|
||||
c.Flags().SetInterspersed(false)
|
||||
@ -660,6 +661,10 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
"profile",
|
||||
completeProfileNames(dockerCli, &opts),
|
||||
)
|
||||
c.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
"progress",
|
||||
cobra.FixedCompletions(printerModes, cobra.ShellCompDirectiveNoFileComp),
|
||||
)
|
||||
|
||||
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
|
||||
c.Flags().IntVar(¶llel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
|
||||
|
@ -50,6 +50,8 @@ type configOptions struct {
|
||||
noResolveEnv bool
|
||||
services bool
|
||||
volumes bool
|
||||
networks bool
|
||||
models bool
|
||||
profiles bool
|
||||
images bool
|
||||
hash string
|
||||
@ -111,6 +113,12 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
if opts.volumes {
|
||||
return runVolumes(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.networks {
|
||||
return runNetworks(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.models {
|
||||
return runModels(ctx, dockerCli, opts)
|
||||
}
|
||||
if opts.hash != "" {
|
||||
return runHash(ctx, dockerCli, opts)
|
||||
}
|
||||
@ -147,6 +155,8 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.")
|
||||
flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.")
|
||||
flags.BoolVar(&opts.networks, "networks", false, "Print the network names, one per line.")
|
||||
flags.BoolVar(&opts.models, "models", false, "Print the model names, one per line.")
|
||||
flags.BoolVar(&opts.profiles, "profiles", false, "Print the profile names, one per line.")
|
||||
flags.BoolVar(&opts.images, "images", false, "Print the image names, one per line.")
|
||||
flags.StringVar(&opts.hash, "hash", "", "Print the service config hash, one per line.")
|
||||
@ -367,6 +377,30 @@ func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for n := range project.Networks {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, model := range project.Models {
|
||||
if model.Model != "" {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), model.Model)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
var services []string
|
||||
if opts.hash != "*" {
|
||||
|
@ -29,7 +29,9 @@ import (
|
||||
|
||||
type eventsOpts struct {
|
||||
*composeOptions
|
||||
json bool
|
||||
json bool
|
||||
since string
|
||||
until string
|
||||
}
|
||||
|
||||
func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
@ -48,6 +50,8 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects")
|
||||
cmd.Flags().StringVar(&opts.since, "since", "", "Show all events created since timestamp")
|
||||
cmd.Flags().StringVar(&opts.until, "until", "", "Stream events until this timestamp")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -59,6 +63,8 @@ func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
|
||||
return backend.Events(ctx, name, api.EventsOptions{
|
||||
Services: services,
|
||||
Since: opts.since,
|
||||
Until: opts.until,
|
||||
Consumer: func(event api.Event) error {
|
||||
if opts.json {
|
||||
marshal, err := json.Marshal(map[string]interface{}{
|
||||
|
@ -18,12 +18,16 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -59,7 +63,15 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runExec(ctx, dockerCli, backend, opts)
|
||||
err := runExec(ctx, dockerCli, backend, opts)
|
||||
if err != nil {
|
||||
logrus.Debugf("%v", err)
|
||||
var cliError cli.StatusError
|
||||
if ok := errors.As(err, &cliError); ok {
|
||||
os.Exit(err.(cli.StatusError).StatusCode) //nolint: errorlint
|
||||
}
|
||||
}
|
||||
return err
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
@ -109,8 +121,8 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, op
|
||||
|
||||
exitCode, err := backend.Exec(ctx, projectName, execOpts)
|
||||
if exitCode != 0 {
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("exit status %d", exitCode)
|
||||
if err != nil && err.Error() != "" {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
return cli.StatusError{StatusCode: exitCode, Status: errMsg}
|
||||
|
@ -88,12 +88,39 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
return nil
|
||||
}
|
||||
if opts.Format == "json" {
|
||||
// Convert map to slice
|
||||
var imageList []api.ImageSummary
|
||||
for _, img := range images {
|
||||
imageList = append(imageList, img)
|
||||
|
||||
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"`
|
||||
}
|
||||
return formatter.Print(imageList, opts.Format, dockerCli.Out(), nil)
|
||||
// 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(),
|
||||
|
@ -271,22 +271,6 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
return err
|
||||
}
|
||||
|
||||
err = progress.Run(ctx, func(ctx context.Context) error {
|
||||
var buildForDeps *api.BuildOptions
|
||||
if !createOpts.noBuild {
|
||||
// allow dependencies needing build to be implicitly selected
|
||||
bo, err := buildOpts.toAPIBuildOptions(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildForDeps = &bo
|
||||
}
|
||||
return startDependencies(ctx, backend, *project, buildForDeps, options)
|
||||
}, dockerCli.Err())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
labels := types.Labels{}
|
||||
for _, s := range options.labels {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
@ -298,9 +282,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
|
||||
var buildForRun *api.BuildOptions
|
||||
if !createOpts.noBuild {
|
||||
// dependencies have already been started above, so only the service
|
||||
// being run might need to be built at this point
|
||||
bo, err := buildOpts.toAPIBuildOptions([]string{options.Service})
|
||||
bo, err := buildOpts.toAPIBuildOptions(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -314,7 +296,12 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
|
||||
// start container and attach to container streams
|
||||
runOpts := api.RunOptions{
|
||||
Build: buildForRun,
|
||||
CreateOptions: api.CreateOptions{
|
||||
Build: buildForRun,
|
||||
RemoveOrphans: options.removeOrphans,
|
||||
IgnoreOrphans: options.ignoreOrphans,
|
||||
QuietPull: options.quietPull,
|
||||
},
|
||||
Name: options.name,
|
||||
Service: options.Service,
|
||||
Command: options.Command,
|
||||
@ -332,7 +319,6 @@ 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 {
|
||||
@ -352,34 +338,3 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func startDependencies(ctx context.Context, backend api.Service, project types.Project, buildOpts *api.BuildOptions, options runOptions) error {
|
||||
dependencies := types.Services{}
|
||||
var requestedService types.ServiceConfig
|
||||
for name, service := range project.Services {
|
||||
if name != options.Service {
|
||||
dependencies[name] = service
|
||||
} else {
|
||||
requestedService = service
|
||||
}
|
||||
}
|
||||
|
||||
project.Services = dependencies
|
||||
project.DisabledServices[options.Service] = requestedService
|
||||
err := backend.Create(ctx, &project, api.CreateOptions{
|
||||
Build: buildOpts,
|
||||
IgnoreOrphans: options.ignoreOrphans,
|
||||
RemoveOrphans: options.removeOrphans,
|
||||
QuietPull: options.quietPull,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(dependencies) > 0 {
|
||||
return backend.Start(ctx, project.Name, api.StartOptions{
|
||||
Project: &project,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -165,6 +165,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *c
|
||||
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
|
||||
flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers")
|
||||
flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information")
|
||||
flags.BoolVar(&build.quiet, "quiet-build", false, "Suppress the build output")
|
||||
flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.")
|
||||
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services")
|
||||
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services")
|
||||
@ -223,6 +224,7 @@ func validateFlags(up *upOptions, create *createOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func runUp(
|
||||
ctx context.Context,
|
||||
dockerCli command.Cli,
|
||||
@ -330,7 +332,7 @@ func runUp(
|
||||
WaitTimeout: timeout,
|
||||
Watch: upOptions.watch,
|
||||
Services: services,
|
||||
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain",
|
||||
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain" && dockerCli.In().IsTerminal(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
95
cmd/compose/volumes.go
Normal file
95
cmd/compose/volumes.go
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type volumesOptions struct {
|
||||
*ProjectOptions
|
||||
Quiet bool
|
||||
Format string
|
||||
}
|
||||
|
||||
func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
options := volumesOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "volumes [OPTIONS] [SERVICE...]",
|
||||
Short: "List volumes",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runVol(ctx, dockerCli, backend, args, options)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display volume names")
|
||||
cmd.Flags().StringVar(&options.Format, "format", "table", flags.FormatHelp)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runVol(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, options volumesOptions) error {
|
||||
project, _, err := options.projectOrName(ctx, dockerCli, services...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
names := project.ServiceNames()
|
||||
|
||||
if len(services) == 0 {
|
||||
services = names
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if !slices.Contains(names, service) {
|
||||
return fmt.Errorf("no such service: %s", service)
|
||||
}
|
||||
}
|
||||
|
||||
volumes, err := backend.Volumes(ctx, project, api.VolumesOptions{
|
||||
Services: services,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if options.Quiet {
|
||||
for _, v := range volumes {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), v.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
volumeCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewVolumeFormat(options.Format, options.Quiet),
|
||||
}
|
||||
|
||||
return formatter.VolumeWrite(volumeCtx, volumes)
|
||||
}
|
@ -28,49 +28,42 @@ func ansi(code string) string {
|
||||
return fmt.Sprintf("\033%s", code)
|
||||
}
|
||||
|
||||
func SaveCursor() {
|
||||
func saveCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("7"))
|
||||
}
|
||||
|
||||
func RestoreCursor() {
|
||||
func restoreCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("8"))
|
||||
}
|
||||
|
||||
func HideCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("[?25l"))
|
||||
}
|
||||
|
||||
func ShowCursor() {
|
||||
func showCursor() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi("[?25h"))
|
||||
}
|
||||
|
||||
func MoveCursor(y, x int) {
|
||||
func moveCursor(y, x int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
|
||||
}
|
||||
|
||||
func MoveCursorX(pos int) {
|
||||
func carriageReturn() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dG", pos)))
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dG", 0)))
|
||||
}
|
||||
|
||||
func ClearLine() {
|
||||
func clearLine() {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
@ -78,7 +71,7 @@ func ClearLine() {
|
||||
fmt.Print(ansi("[2K"))
|
||||
}
|
||||
|
||||
func MoveCursorUp(lines int) {
|
||||
func moveCursorUp(lines int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
@ -86,7 +79,7 @@ func MoveCursorUp(lines int) {
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
|
||||
}
|
||||
|
||||
func MoveCursorDown(lines int) {
|
||||
func moveCursorDown(lines int) {
|
||||
if disableAnsi {
|
||||
return
|
||||
}
|
||||
@ -94,7 +87,7 @@ func MoveCursorDown(lines int) {
|
||||
fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
|
||||
}
|
||||
|
||||
func NewLine() {
|
||||
func newLine() {
|
||||
// Like \n
|
||||
fmt.Print("\012")
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package formatter
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -58,6 +59,9 @@ const (
|
||||
Auto = "auto"
|
||||
)
|
||||
|
||||
// ansiColorOffset is the offset for basic foreground colors in ANSI escape codes.
|
||||
const ansiColorOffset = 30
|
||||
|
||||
// SetANSIMode configure formatter for colored output on ANSI-compliant console
|
||||
func SetANSIMode(streams command.Streams, ansi string) {
|
||||
if !useAnsi(streams, ansi) {
|
||||
@ -91,11 +95,15 @@ func ansiColor(code, s string, formatOpts ...string) string {
|
||||
|
||||
// Everything about ansiColorCode color https://hyperskill.org/learn/step/18193
|
||||
func ansiColorCode(code string, formatOpts ...string) string {
|
||||
res := "\033["
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\033[")
|
||||
for _, c := range formatOpts {
|
||||
res = fmt.Sprintf("%s%s;", res, c)
|
||||
sb.WriteString(c)
|
||||
sb.WriteString(";")
|
||||
}
|
||||
return fmt.Sprintf("%s%sm", res, code)
|
||||
sb.WriteString(code)
|
||||
sb.WriteString("m")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func makeColorFunc(code string) colorFunc {
|
||||
@ -122,8 +130,8 @@ func rainbowColor() colorFunc {
|
||||
func init() {
|
||||
colors := map[string]colorFunc{}
|
||||
for i, name := range names {
|
||||
colors[name] = makeColorFunc(strconv.Itoa(30 + i))
|
||||
colors["intense_"+name] = makeColorFunc(strconv.Itoa(30+i) + ";1")
|
||||
colors[name] = makeColorFunc(strconv.Itoa(ansiColorOffset + i))
|
||||
colors["intense_"+name] = makeColorFunc(strconv.Itoa(ansiColorOffset+i) + ";1")
|
||||
}
|
||||
rainbow = []colorFunc{
|
||||
colors["cyan"],
|
||||
|
@ -56,10 +56,6 @@ func NewLogConsumer(ctx context.Context, stdout, stderr io.Writer, color, prefix
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logConsumer) Register(name string) {
|
||||
l.register(name)
|
||||
}
|
||||
|
||||
func (l *logConsumer) register(name string) *presenter {
|
||||
var p *presenter
|
||||
root, _, found := strings.Cut(name, " ")
|
||||
@ -73,9 +69,12 @@ func (l *logConsumer) register(name string) *presenter {
|
||||
} else {
|
||||
cf := monochrome
|
||||
if l.color {
|
||||
if name == api.WatchLogger {
|
||||
switch name {
|
||||
case "":
|
||||
cf = monochrome
|
||||
case api.WatchLogger:
|
||||
cf = makeColorFunc("92")
|
||||
} else {
|
||||
default:
|
||||
cf = nextColor()
|
||||
}
|
||||
}
|
||||
@ -118,23 +117,15 @@ func (l *logConsumer) write(w io.Writer, container, message string) {
|
||||
if l.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if KeyboardManager != nil {
|
||||
KeyboardManager.ClearKeyboardInfo()
|
||||
}
|
||||
|
||||
p := l.getPresenter(container)
|
||||
timestamp := time.Now().Format(jsonmessage.RFC3339NanoFixed)
|
||||
for _, line := range strings.Split(message, "\n") {
|
||||
if l.timestamp {
|
||||
_, _ = fmt.Fprintf(w, "%s%s%s\n", p.prefix, timestamp, line)
|
||||
_, _ = fmt.Fprintf(w, "%s%s %s\n", p.prefix, timestamp, line)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, "%s%s\n", p.prefix, line)
|
||||
}
|
||||
}
|
||||
|
||||
if KeyboardManager != nil {
|
||||
KeyboardManager.PrintKeyboardInfo()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logConsumer) Status(container, msg string) {
|
||||
@ -168,3 +159,27 @@ func (p *presenter) setPrefix(width int) {
|
||||
}
|
||||
p.prefix = p.colors(fmt.Sprintf("%-"+strconv.Itoa(width)+"s | ", p.name))
|
||||
}
|
||||
|
||||
type logDecorator struct {
|
||||
decorated api.LogConsumer
|
||||
Before func()
|
||||
After func()
|
||||
}
|
||||
|
||||
func (l logDecorator) Log(containerName, message string) {
|
||||
l.Before()
|
||||
l.decorated.Log(containerName, message)
|
||||
l.After()
|
||||
}
|
||||
|
||||
func (l logDecorator) Err(containerName, message string) {
|
||||
l.Before()
|
||||
l.decorated.Err(containerName, message)
|
||||
l.After()
|
||||
}
|
||||
|
||||
func (l logDecorator) Status(container, msg string) {
|
||||
l.Before()
|
||||
l.decorated.Status(container, msg)
|
||||
l.After()
|
||||
}
|
||||
|
@ -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,10 +70,11 @@ func (ke *KeyboardError) error() string {
|
||||
|
||||
type KeyboardWatch struct {
|
||||
Watching bool
|
||||
Watcher Toggle
|
||||
Watcher Feature
|
||||
}
|
||||
|
||||
type Toggle interface {
|
||||
// Feature is an compose feature that can be started/stopped by a menu command
|
||||
type Feature interface {
|
||||
Start(context.Context) error
|
||||
Stop() error
|
||||
}
|
||||
@ -88,32 +89,26 @@ const (
|
||||
|
||||
type LogKeyboard struct {
|
||||
kError KeyboardError
|
||||
Watch KeyboardWatch
|
||||
Watch *KeyboardWatch
|
||||
IsDockerDesktopActive bool
|
||||
IsWatchConfigured bool
|
||||
logLevel KEYBOARD_LOG_LEVEL
|
||||
signalChannel chan<- os.Signal
|
||||
}
|
||||
|
||||
// FIXME(ndeloof) we should avoid use of such a global reference. see use in logConsumer
|
||||
var KeyboardManager *LogKeyboard
|
||||
|
||||
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal, w bool, watcher Toggle) *LogKeyboard {
|
||||
KeyboardManager = &LogKeyboard{
|
||||
Watch: KeyboardWatch{
|
||||
Watching: w,
|
||||
Watcher: watcher,
|
||||
},
|
||||
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal) *LogKeyboard {
|
||||
return &LogKeyboard{
|
||||
IsDockerDesktopActive: isDockerDesktopActive,
|
||||
IsWatchConfigured: true,
|
||||
logLevel: INFO,
|
||||
signalChannel: sc,
|
||||
}
|
||||
return KeyboardManager
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) ClearKeyboardInfo() {
|
||||
lk.clearNavigationMenu()
|
||||
func (lk *LogKeyboard) Decorate(l api.LogConsumer) api.LogConsumer {
|
||||
return logDecorator{
|
||||
decorated: l,
|
||||
Before: lk.clearNavigationMenu,
|
||||
After: lk.PrintKeyboardInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) PrintKeyboardInfo() {
|
||||
@ -138,7 +133,7 @@ func (lk *LogKeyboard) createBuffer(lines int) {
|
||||
|
||||
if lines > 0 {
|
||||
allocateSpace(lines)
|
||||
MoveCursorUp(lines)
|
||||
moveCursorUp(lines)
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,17 +146,17 @@ func (lk *LogKeyboard) printNavigationMenu() {
|
||||
height := goterm.Height()
|
||||
menu := lk.navigationMenu()
|
||||
|
||||
MoveCursorX(0)
|
||||
SaveCursor()
|
||||
carriageReturn()
|
||||
saveCursor()
|
||||
|
||||
lk.kError.printError(height, menu)
|
||||
|
||||
MoveCursor(height-extraLines(menu), 0)
|
||||
ClearLine()
|
||||
moveCursor(height-extraLines(menu), 0)
|
||||
clearLine()
|
||||
fmt.Print(menu)
|
||||
|
||||
MoveCursorX(0)
|
||||
RestoreCursor()
|
||||
carriageReturn()
|
||||
restoreCursor()
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,7 +179,7 @@ func (lk *LogKeyboard) navigationMenu() string {
|
||||
watchInfo = navColor(" ")
|
||||
}
|
||||
isEnabled := " Enable"
|
||||
if lk.Watch.Watching {
|
||||
if lk.Watch != nil && lk.Watch.Watching {
|
||||
isEnabled = " Disable"
|
||||
}
|
||||
watchInfo = watchInfo + shortcutKeyColor("w") + navColor(isEnabled+" Watch")
|
||||
@ -193,15 +188,15 @@ func (lk *LogKeyboard) navigationMenu() string {
|
||||
|
||||
func (lk *LogKeyboard) clearNavigationMenu() {
|
||||
height := goterm.Height()
|
||||
MoveCursorX(0)
|
||||
SaveCursor()
|
||||
carriageReturn()
|
||||
saveCursor()
|
||||
|
||||
// ClearLine()
|
||||
// clearLine()
|
||||
for i := 0; i < height; i++ {
|
||||
MoveCursorDown(1)
|
||||
ClearLine()
|
||||
moveCursorDown(1)
|
||||
clearLine()
|
||||
}
|
||||
RestoreCursor()
|
||||
restoreCursor()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
|
||||
@ -267,7 +262,7 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
|
||||
if !lk.IsWatchConfigured {
|
||||
if lk.Watch == nil {
|
||||
return
|
||||
}
|
||||
if lk.Watch.Watching {
|
||||
@ -298,7 +293,7 @@ func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEv
|
||||
case 'v':
|
||||
lk.openDockerDesktop(ctx, project)
|
||||
case 'w':
|
||||
if !lk.IsWatchConfigured {
|
||||
if lk.Watch == nil {
|
||||
// we try to open watch docs if DD is installed
|
||||
if lk.IsDockerDesktopActive {
|
||||
lk.openDDWatchDocs(ctx, project)
|
||||
@ -321,22 +316,29 @@ func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEv
|
||||
case keyboard.KeyCtrlC:
|
||||
_ = keyboard.Close()
|
||||
lk.clearNavigationMenu()
|
||||
ShowCursor()
|
||||
showCursor()
|
||||
|
||||
lk.logLevel = NONE
|
||||
// will notify main thread to kill and will handle gracefully
|
||||
lk.signalChannel <- syscall.SIGINT
|
||||
case keyboard.KeyEnter:
|
||||
NewLine()
|
||||
newLine()
|
||||
lk.printNavigationMenu()
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) EnableWatch(enabled bool, watcher Feature) {
|
||||
lk.Watch = &KeyboardWatch{
|
||||
Watching: enabled,
|
||||
Watcher: watcher,
|
||||
}
|
||||
}
|
||||
|
||||
func allocateSpace(lines int) {
|
||||
for i := 0; i < lines; i++ {
|
||||
ClearLine()
|
||||
NewLine()
|
||||
MoveCursorX(0)
|
||||
clearLine()
|
||||
newLine()
|
||||
carriageReturn()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
"os"
|
||||
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/cmd/cmdtrace"
|
||||
@ -68,7 +68,7 @@ func pluginMain() {
|
||||
})
|
||||
return cmd
|
||||
},
|
||||
manager.Metadata{
|
||||
metadata.Metadata{
|
||||
SchemaVersion: "0.1.0",
|
||||
Vendor: "Docker Inc.",
|
||||
Version: internal.Version,
|
||||
|
@ -39,20 +39,30 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
type options struct {
|
||||
db string
|
||||
size int
|
||||
}
|
||||
|
||||
func composeCommand() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "compose EVENT",
|
||||
TraverseChildren: true,
|
||||
}
|
||||
c.PersistentFlags().String("project-name", "", "compose project name") // unused
|
||||
|
||||
var options options
|
||||
upCmd := &cobra.Command{
|
||||
Use: "up",
|
||||
Run: up,
|
||||
Use: "up",
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
up(options, args)
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
upCmd.Flags().String("type", "", "Database type (mysql, postgres, etc.)")
|
||||
|
||||
upCmd.Flags().StringVar(&options.db, "type", "", "Database type (mysql, postgres, etc.)")
|
||||
_ = upCmd.MarkFlagRequired("type")
|
||||
upCmd.Flags().Int("size", 10, "Database size in GB")
|
||||
upCmd.Flags().IntVar(&options.size, "size", 10, "Database size in GB")
|
||||
upCmd.Flags().String("name", "", "Name of the database to be created")
|
||||
_ = upCmd.MarkFlagRequired("name")
|
||||
|
||||
@ -71,13 +81,13 @@ func composeCommand() *cobra.Command {
|
||||
|
||||
const lineSeparator = "\n"
|
||||
|
||||
func up(_ *cobra.Command, args []string) {
|
||||
func up(options options, args []string) {
|
||||
servicename := args[0]
|
||||
fmt.Printf(`{ "type": "debug", "message": "Starting %s" }%s`, servicename, lineSeparator)
|
||||
|
||||
for i := 0; i < 100; i += 10 {
|
||||
for i := 0; i < options.size; i += 10 {
|
||||
time.Sleep(1 * time.Second)
|
||||
fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i, lineSeparator)
|
||||
fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i*100/options.size, lineSeparator)
|
||||
}
|
||||
fmt.Printf(`{ "type": "setenv", "message": "URL=https://magic.cloud/%s" }%s`, servicename, lineSeparator)
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ Define and run multi-container applications with Docker
|
||||
| [`unpause`](compose_unpause.md) | Unpause services |
|
||||
| [`up`](compose_up.md) | Create and start containers |
|
||||
| [`version`](compose_version.md) | Show the Docker Compose version information |
|
||||
| [`volumes`](compose_volumes.md) | List volumes |
|
||||
| [`wait`](compose_wait.md) | Block until containers of all (or specified) services stop. |
|
||||
| [`watch`](compose_watch.md) | Watch build context for service and rebuild/refresh containers when files are updated |
|
||||
|
||||
|
@ -22,9 +22,11 @@ run `docker compose build` to rebuild it.
|
||||
| `-m`, `--memory` | `bytes` | `0` | Set memory limit for the build container. Not supported by BuildKit. |
|
||||
| `--no-cache` | `bool` | | Do not use cache when building the image |
|
||||
| `--print` | `bool` | | Print equivalent bake file |
|
||||
| `--provenance` | `string` | | Add a provenance attestation |
|
||||
| `--pull` | `bool` | | Always attempt to pull a newer version of the image |
|
||||
| `--push` | `bool` | | Push service images |
|
||||
| `-q`, `--quiet` | `bool` | | Don't print anything to STDOUT |
|
||||
| `-q`, `--quiet` | `bool` | | Suppress the build output |
|
||||
| `--sbom` | `string` | | Add a SBOM attestation |
|
||||
| `--ssh` | `string` | | Set SSH authentications used when building service images. (use 'default' for using your default SSH Agent) |
|
||||
| `--with-dependencies` | `bool` | | Also build dependencies (transitively) |
|
||||
|
||||
|
@ -15,6 +15,8 @@ the canonical format.
|
||||
| `--hash` | `string` | | Print the service config hash, one per line. |
|
||||
| `--images` | `bool` | | Print the image names, one per line. |
|
||||
| `--lock-image-digests` | `bool` | | Produces an override file with image digests |
|
||||
| `--models` | `bool` | | Print the model names, one per line. |
|
||||
| `--networks` | `bool` | | Print the network names, one per line. |
|
||||
| `--no-consistency` | `bool` | | Don't check model consistency - warning: may produce invalid Compose output |
|
||||
| `--no-env-resolution` | `bool` | | Don't resolve service env files |
|
||||
| `--no-interpolate` | `bool` | | Don't interpolate environment variables |
|
||||
|
@ -23,10 +23,12 @@ The events that can be received using this can be seen [here](/reference/cli/doc
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:------------|:-------|:--------|:------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--json` | `bool` | | Output events as a stream of json objects |
|
||||
| Name | Type | Default | Description |
|
||||
|:------------|:---------|:--------|:------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--json` | `bool` | | Output events as a stream of json objects |
|
||||
| `--since` | `string` | | Show all events created since timestamp |
|
||||
| `--until` | `string` | | Stream events until this timestamp |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
@ -44,6 +44,7 @@ If the process is interrupted using `SIGINT` (ctrl + C) or `SIGTERM`, the contai
|
||||
| `--no-recreate` | `bool` | | If containers already exist, don't recreate them. Incompatible with --force-recreate. |
|
||||
| `--no-start` | `bool` | | Don't start the services after creating them |
|
||||
| `--pull` | `string` | `policy` | Pull image before running ("always"\|"missing"\|"never") |
|
||||
| `--quiet-build` | `bool` | | Suppress the build output |
|
||||
| `--quiet-pull` | `bool` | | Pull without printing progress information |
|
||||
| `--remove-orphans` | `bool` | | Remove containers for services not defined in the Compose file |
|
||||
| `-V`, `--renew-anon-volumes` | `bool` | | Recreate anonymous volumes instead of retrieving data from the previous containers |
|
||||
|
16
docs/reference/compose_volumes.md
Normal file
16
docs/reference/compose_volumes.md
Normal file
@ -0,0 +1,16 @@
|
||||
# docker compose volumes
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
List volumes
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--format` | `string` | `table` | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
|
||||
| `-q`, `--quiet` | `bool` | | Only display volume names |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
@ -37,6 +37,7 @@ cname:
|
||||
- docker compose unpause
|
||||
- docker compose up
|
||||
- docker compose version
|
||||
- docker compose volumes
|
||||
- docker compose wait
|
||||
- docker compose watch
|
||||
clink:
|
||||
@ -72,6 +73,7 @@ clink:
|
||||
- docker_compose_unpause.yaml
|
||||
- docker_compose_up.yaml
|
||||
- docker_compose_version.yaml
|
||||
- docker_compose_volumes.yaml
|
||||
- docker_compose_wait.yaml
|
||||
- docker_compose_watch.yaml
|
||||
options:
|
||||
|
@ -125,6 +125,15 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: provenance
|
||||
value_type: string
|
||||
description: Add a provenance attestation
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: pull
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
@ -149,7 +158,16 @@ options:
|
||||
shorthand: q
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Don't print anything to STDOUT
|
||||
description: Suppress the build output
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: sbom
|
||||
value_type: string
|
||||
description: Add a SBOM attestation
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
|
@ -56,6 +56,26 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: models
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Print the model names, one per line.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: networks
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Print the network names, one per line.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: no-consistency
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
|
@ -34,6 +34,24 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: since
|
||||
value_type: string
|
||||
description: Show all events created since timestamp
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: until
|
||||
value_type: string
|
||||
description: Stream events until this timestamp
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
|
@ -211,6 +211,16 @@ options:
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: quiet-build
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Suppress the build output
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: quiet-pull
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
|
52
docs/reference/docker_compose_volumes.yaml
Normal file
52
docs/reference/docker_compose_volumes.yaml
Normal file
@ -0,0 +1,52 @@
|
||||
command: docker compose volumes
|
||||
short: List volumes
|
||||
long: List volumes
|
||||
usage: docker compose volumes [OPTIONS] [SERVICE...]
|
||||
pname: docker compose
|
||||
plink: docker_compose.yaml
|
||||
options:
|
||||
- option: format
|
||||
value_type: string
|
||||
default_value: table
|
||||
description: |-
|
||||
Format output using a custom template:
|
||||
'table': Print output in table format with column headers (default)
|
||||
'table TEMPLATE': Print output in table format using the given Go template
|
||||
'json': Print in JSON format
|
||||
'TEMPLATE': Print output using the given Go template.
|
||||
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: quiet
|
||||
shorthand: q
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Only display volume names
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Execute command in dry run mode
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
82
go.mod
82
go.mod
@ -1,6 +1,6 @@
|
||||
module github.com/docker/compose/v2
|
||||
|
||||
go 1.23.8
|
||||
go 1.23.12
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
@ -8,28 +8,27 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/compose-spec/compose-go/v2 v2.6.4
|
||||
github.com/containerd/containerd/v2 v2.1.1
|
||||
github.com/compose-spec/compose-go/v2 v2.8.1
|
||||
github.com/containerd/containerd/v2 v2.1.4
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/containerd/platforms v1.0.0-rc.1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/buildx v0.24.0
|
||||
github.com/docker/cli v28.2.2+incompatible
|
||||
github.com/docker/buildx v0.28.0-rc1
|
||||
github.com/docker/cli v28.3.3+incompatible
|
||||
github.com/docker/cli-docs-tool v0.10.0
|
||||
github.com/docker/docker v28.2.2+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/docker v28.3.3+incompatible
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
|
||||
github.com/fsnotify/fsevents v0.2.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/jonboulle/clockwork v0.5.0
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/moby/buildkit v0.22.0
|
||||
github.com/moby/buildkit v0.24.0-rc1
|
||||
github.com/moby/go-archive v0.1.0
|
||||
github.com/moby/patternmatcher v0.6.0
|
||||
github.com/moby/sys/atomicwriter v0.1.0
|
||||
@ -41,21 +40,21 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel v1.36.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
|
||||
go.opentelemetry.io/otel/metric v1.35.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
go.opentelemetry.io/otel/metric v1.36.0
|
||||
go.opentelemetry.io/otel/sdk v1.36.0
|
||||
go.opentelemetry.io/otel/trace v1.36.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.uber.org/mock v0.5.2
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/sys v0.33.0
|
||||
google.golang.org/grpc v1.73.0
|
||||
go.uber.org/mock v0.6.0
|
||||
golang.org/x/sync v0.16.0
|
||||
golang.org/x/sys v0.35.0
|
||||
google.golang.org/grpc v1.74.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
tags.cncf.io/container-device-interface v1.0.1
|
||||
@ -64,7 +63,7 @@ require (
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
@ -82,14 +81,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.4 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/containerd/containerd/api v1.9.0 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/containerd/errdefs/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.6 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
@ -98,12 +97,12 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
@ -117,7 +116,8 @@ 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/in-toto/in-toto-golang v0.5.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
@ -157,13 +157,14 @@ 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/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250417144416-3f76f8130144 // indirect
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f // indirect
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@ -174,20 +175,21 @@ require (
|
||||
github.com/zclconf/go-cty v1.16.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
@ -204,7 +206,7 @@ require (
|
||||
)
|
||||
|
||||
exclude (
|
||||
// FIXME(thaJeztah): remoove this once kubernetes updated their dependencies to no longer need this.
|
||||
// FIXME(thaJeztah): remove this once kubernetes updated their dependencies to no longer need this.
|
||||
//
|
||||
// For additional details, see this PR and links mentioned in that PR:
|
||||
// https://github.com/kubernetes-sigs/kustomize/pull/5830#issuecomment-2569960859
|
||||
|
172
go.sum
172
go.sum
@ -10,8 +10,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
|
||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=
|
||||
@ -80,16 +80,16 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/compose-spec/compose-go/v2 v2.6.4 h1:Gjv6x8eAhqwwWvoXIo0oZ4bDQBh0OMwdU7LUL9PDLiM=
|
||||
github.com/compose-spec/compose-go/v2 v2.6.4/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA=
|
||||
github.com/compose-spec/compose-go/v2 v2.8.1 h1:27O4dzyhiS/UEUKp1zHOHCBWD1WbxGsYGMNNaSejTk4=
|
||||
github.com/compose-spec/compose-go/v2 v2.8.1/go.mod h1:veko/VB7URrg/tKz3vmIAQDaz+CGiXH8vZsW79NmAww=
|
||||
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
|
||||
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
|
||||
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
||||
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
|
||||
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0=
|
||||
github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI=
|
||||
github.com/containerd/containerd/v2 v2.1.1 h1:znnkm7Ajz8lg8BcIPMhc/9yjBRN3B+OkNKqKisKfwwM=
|
||||
github.com/containerd/containerd/v2 v2.1.1/go.mod h1:zIfkQj4RIodclYQkX7GSSswSwgP8d/XxDOtOAoSDIGU=
|
||||
github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ=
|
||||
github.com/containerd/containerd/v2 v2.1.4/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
|
||||
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@ -100,8 +100,8 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
|
||||
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/nydus-snapshotter v0.15.0 h1:RqZRs1GPeM6T3wmuxJV9u+2Rg4YETVMwTmiDeX+iWC8=
|
||||
github.com/containerd/nydus-snapshotter v0.15.0/go.mod h1:biq0ijpeZe0I5yZFSJyHzFSjjRZQ7P7y/OuHyd7hYOw=
|
||||
github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg=
|
||||
github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM=
|
||||
github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E=
|
||||
github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
|
||||
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
|
||||
@ -113,8 +113,9 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq
|
||||
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
|
||||
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
|
||||
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
@ -125,24 +126,26 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/buildx v0.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/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-rc1 h1:XQ0hwhIB+Jf211iBdts30WtsqIQ8/xY7/QcYDzfDFEY=
|
||||
github.com/docker/buildx v0.28.0-rc1/go.mod h1:LGGGhqW6EbnC96hK9XSVh4h7kI+G/bwTgJJh/uAuQWo=
|
||||
github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
|
||||
github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
|
||||
github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
|
||||
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
||||
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
||||
@ -169,8 +172,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
@ -186,8 +189,8 @@ github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@ -216,8 +219,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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@ -243,8 +246,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.5.0 h1:hb8bgwr0M2hGdDsLjkJ3ZqJ8JFLL/tgYdAxF/XEFBbY=
|
||||
github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1GdHMCq8+WPxw8/BE=
|
||||
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
|
||||
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
@ -315,8 +318,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z
|
||||
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/buildkit v0.22.0 h1:aWN06w1YGSVN1XfeZbj2ZbgY+zi5xDAjEFI8Cy9fTjA=
|
||||
github.com/moby/buildkit v0.22.0/go.mod h1:j4pP5hxiTWcz7xuTK2cyxQislHl/N2WWHzOy43DlLJw=
|
||||
github.com/moby/buildkit v0.24.0-rc1 h1:taA+MPeYWtGyRQ1SGbdTHVk5khWKFR7f9WI2coW/Ggs=
|
||||
github.com/moby/buildkit v0.24.0-rc1/go.mod h1:4qovICAdR2H4C7+EGMRva5zgHW1gyhT4/flHI7F5F9k=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
@ -423,8 +426,10 @@ 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/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
|
||||
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
|
||||
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
||||
@ -436,8 +441,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
|
||||
github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
|
||||
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
|
||||
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
|
||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk=
|
||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
@ -446,8 +451,9 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c h1:2EejZtjFjKJGk71ANb+wtFK5EjUzUkEM3R0xnp559xg=
|
||||
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -464,18 +470,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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
|
||||
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
|
||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
|
||||
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-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/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
|
||||
@ -504,36 +510,38 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@ -542,13 +550,13 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@ -558,10 +566,10 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -569,8 +577,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -592,19 +600,19 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -612,19 +620,19 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
|
@ -1,4 +1,4 @@
|
||||
//go:build linux || openbsd
|
||||
//go:build linux || openbsd || freebsd
|
||||
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
38
internal/registry/registry.go
Normal file
38
internal/registry/registry.go
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/distribution/reference"
|
||||
|
||||
const (
|
||||
// IndexHostname is the index hostname, used for authentication and image search.
|
||||
IndexHostname = "index.docker.io"
|
||||
// IndexServer is used for user auth and image search
|
||||
IndexServer = "https://index.docker.io/v1/"
|
||||
// IndexName is the name of the index
|
||||
IndexName = "docker.io"
|
||||
)
|
||||
|
||||
// GetAuthConfigKey special-cases using the full index address of the official
|
||||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
||||
func GetAuthConfigKey(reposName reference.Named) string {
|
||||
indexName := reference.Domain(reposName)
|
||||
if indexName == IndexName || indexName == IndexHostname {
|
||||
return IndexServer
|
||||
}
|
||||
return indexName
|
||||
}
|
@ -29,11 +29,11 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/go-archive"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type archiveEntry struct {
|
||||
@ -84,7 +84,14 @@ func (t *Tar) Sync(ctx context.Context, service string, paths []*PathMapping) er
|
||||
if len(pathsToDelete) != 0 {
|
||||
deleteCmd = append([]string{"rm", "-rf"}, pathsToDelete...)
|
||||
}
|
||||
var eg multierror.Group
|
||||
|
||||
var (
|
||||
eg errgroup.Group
|
||||
errMu sync.Mutex
|
||||
errs = make([]error, 0, len(containers)*2) // max 2 errs per container
|
||||
)
|
||||
|
||||
eg.SetLimit(16) // arbitrary limit, adjust to taste :D
|
||||
for i := range containers {
|
||||
containerID := containers[i].ID
|
||||
tarReader := tarArchive(pathsToCopy)
|
||||
@ -92,17 +99,23 @@ func (t *Tar) Sync(ctx context.Context, service string, paths []*PathMapping) er
|
||||
eg.Go(func() error {
|
||||
if len(deleteCmd) != 0 {
|
||||
if err := t.client.Exec(ctx, containerID, deleteCmd, nil); err != nil {
|
||||
return fmt.Errorf("deleting paths in %s: %w", containerID, err)
|
||||
errMu.Lock()
|
||||
errs = append(errs, fmt.Errorf("deleting paths in %s: %w", containerID, err))
|
||||
errMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
if err := t.client.Untar(ctx, containerID, tarReader); err != nil {
|
||||
return fmt.Errorf("copying files to %s: %w", containerID, err)
|
||||
errMu.Lock()
|
||||
errs = append(errs, fmt.Errorf("copying files to %s: %w", containerID, err))
|
||||
errMu.Unlock()
|
||||
}
|
||||
return nil
|
||||
return nil // don't fail-fast; collect all errors
|
||||
})
|
||||
}
|
||||
return eg.Wait().ErrorOrNil()
|
||||
|
||||
_ = eg.Wait()
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
type ArchiveBuilder struct {
|
||||
@ -229,7 +242,7 @@ func (a *ArchiveBuilder) writeEntry(entry archiveEntry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tarPath writes the given source path into tarWriter at the given dest (recursively for directories).
|
||||
// entriesForPath writes the given source path into tarWriter at the given dest (recursively for directories).
|
||||
// e.g. tarring my_dir --> dest d: d/file_a, d/file_b
|
||||
// If source path does not exist, quietly skips it and returns no err
|
||||
func (a *ArchiveBuilder) entriesForPath(localPath, containerPath string) ([]archiveEntry, error) {
|
||||
|
@ -77,11 +77,13 @@ func ProjectOptions(ctx context.Context, proj *types.Project) SpanOptions {
|
||||
attribute.StringSlice("project.networks", proj.NetworkNames()),
|
||||
attribute.StringSlice("project.secrets", proj.SecretNames()),
|
||||
attribute.StringSlice("project.configs", proj.ConfigNames()),
|
||||
attribute.StringSlice("project.models", proj.ModelNames()),
|
||||
attribute.StringSlice("project.extensions", keys(proj.Extensions)),
|
||||
attribute.StringSlice("project.services.active", proj.ServiceNames()),
|
||||
attribute.StringSlice("project.services.disabled", proj.DisabledServiceNames()),
|
||||
attribute.StringSlice("project.services.build", proj.ServicesWithBuild()),
|
||||
attribute.StringSlice("project.services.depends_on", proj.ServicesWithDependsOn()),
|
||||
attribute.StringSlice("project.services.models", proj.ServicesWithModels()),
|
||||
attribute.StringSlice("project.services.capabilities", capabilities),
|
||||
attribute.StringSlice("project.services.capabilities.gpu", gpu),
|
||||
attribute.StringSlice("project.services.capabilities.tpu", tpu),
|
||||
@ -110,6 +112,7 @@ func ServiceOptions(service types.ServiceConfig) SpanOptions {
|
||||
attribute.String("service.name", service.Name),
|
||||
attribute.String("service.image", service.Image),
|
||||
attribute.StringSlice("service.networks", keys(service.Networks)),
|
||||
attribute.StringSlice("service.models", keys(service.Models)),
|
||||
}
|
||||
|
||||
configNames := make([]string, len(service.Configs))
|
||||
|
@ -22,15 +22,12 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
func KeyboardMetrics(ctx context.Context, enabled, isDockerDesktopActive, isWatchConfigured bool) {
|
||||
func KeyboardMetrics(ctx context.Context, enabled, isDockerDesktopActive bool) {
|
||||
commandAvailable := []string{}
|
||||
if isDockerDesktopActive {
|
||||
commandAvailable = append(commandAvailable, "gui")
|
||||
commandAvailable = append(commandAvailable, "gui/composeview")
|
||||
}
|
||||
if isWatchConfigured {
|
||||
commandAvailable = append(commandAvailable, "watch")
|
||||
}
|
||||
|
||||
AddAttributeToSpan(ctx,
|
||||
attribute.Bool("navmenu.enabled", enabled),
|
||||
|
@ -18,8 +18,9 @@ package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
)
|
||||
|
||||
@ -28,23 +29,45 @@ type MuxExporter struct {
|
||||
}
|
||||
|
||||
func (m MuxExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
|
||||
var eg multierror.Group
|
||||
for i := range m.exporters {
|
||||
exporter := m.exporters[i]
|
||||
eg.Go(func() error {
|
||||
return exporter.ExportSpans(ctx, spans)
|
||||
})
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
errMu sync.Mutex
|
||||
errs = make([]error, 0, len(m.exporters))
|
||||
)
|
||||
|
||||
for _, exporter := range m.exporters {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := exporter.ExportSpans(ctx, spans); err != nil {
|
||||
errMu.Lock()
|
||||
errs = append(errs, err)
|
||||
errMu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
return eg.Wait()
|
||||
wg.Wait()
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (m MuxExporter) Shutdown(ctx context.Context) error {
|
||||
var eg multierror.Group
|
||||
for i := range m.exporters {
|
||||
exporter := m.exporters[i]
|
||||
eg.Go(func() error {
|
||||
return exporter.Shutdown(ctx)
|
||||
})
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
errMu sync.Mutex
|
||||
errs = make([]error, 0, len(m.exporters))
|
||||
)
|
||||
|
||||
for _, exporter := range m.exporters {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := exporter.Shutdown(ctx); err != nil {
|
||||
errMu.Lock()
|
||||
errs = append(errs, err)
|
||||
errMu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
return eg.Wait()
|
||||
wg.Wait()
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@ -26,6 +27,7 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
)
|
||||
|
||||
// Service manages a compose project
|
||||
@ -98,8 +100,16 @@ type Service interface {
|
||||
Commit(ctx context.Context, projectName string, options CommitOptions) error
|
||||
// Generate generates a Compose Project from existing containers
|
||||
Generate(ctx context.Context, options GenerateOptions) (*types.Project, error)
|
||||
// Volumes executes the equivalent to a `docker volume ls`
|
||||
Volumes(ctx context.Context, project *types.Project, options VolumesOptions) ([]VolumesSummary, error)
|
||||
}
|
||||
|
||||
type VolumesOptions struct {
|
||||
Services []string
|
||||
}
|
||||
|
||||
type VolumesSummary = *volume.Volume
|
||||
|
||||
type ScaleOptions struct {
|
||||
Services []string
|
||||
}
|
||||
@ -161,8 +171,14 @@ type BuildOptions struct {
|
||||
Print bool
|
||||
// Check let builder validate build configuration
|
||||
Check bool
|
||||
// Provenance
|
||||
Provenance bool
|
||||
// Attestations allows to enable attestations generation
|
||||
Attestations bool
|
||||
// Provenance generate a provenance attestation
|
||||
Provenance string
|
||||
// SBOM generate a SBOM attestation
|
||||
SBOM string
|
||||
// Out is the stream to write build progress
|
||||
Out io.Writer
|
||||
}
|
||||
|
||||
// Apply mutates project according to build options
|
||||
@ -351,7 +367,7 @@ type RemoveOptions struct {
|
||||
|
||||
// RunOptions group options of the Run API
|
||||
type RunOptions struct {
|
||||
Build *BuildOptions
|
||||
CreateOptions
|
||||
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
|
||||
Project *types.Project
|
||||
Name string
|
||||
@ -371,8 +387,6 @@ type RunOptions struct {
|
||||
Privileged bool
|
||||
UseNetworkAliases bool
|
||||
NoDeps bool
|
||||
// QuietPull makes the pulling process quiet
|
||||
QuietPull bool
|
||||
// used by exec
|
||||
Index int
|
||||
}
|
||||
@ -391,6 +405,8 @@ type AttachOptions struct {
|
||||
type EventsOptions struct {
|
||||
Services []string
|
||||
Consumer func(event Event) error
|
||||
Since string
|
||||
Until string
|
||||
}
|
||||
|
||||
// Event is a container runtime event served by Events API
|
||||
@ -542,6 +558,7 @@ type ImageSummary struct {
|
||||
Tag string
|
||||
Platform platforms.Platform
|
||||
Size int64
|
||||
Created time.Time
|
||||
LastTagTime time.Time
|
||||
}
|
||||
|
||||
@ -640,7 +657,6 @@ type LogConsumer interface {
|
||||
Log(containerName, message string)
|
||||
Err(containerName, message string)
|
||||
Status(container, msg string)
|
||||
Register(container string)
|
||||
}
|
||||
|
||||
// ContainerEventListener is a callback to process ContainerEvent from services
|
||||
@ -648,16 +664,18 @@ type ContainerEventListener func(event ContainerEvent)
|
||||
|
||||
// ContainerEvent notify an event has been collected on source container implementing Service
|
||||
type ContainerEvent struct {
|
||||
Type int
|
||||
// Container is the name of the container _without the project prefix_.
|
||||
Type int
|
||||
Time int64
|
||||
Container *ContainerSummary
|
||||
// Source is the name of the container _without the project prefix_.
|
||||
//
|
||||
// This is only suitable for display purposes within Compose, as it's
|
||||
// not guaranteed to be unique across services.
|
||||
Container string
|
||||
ID string
|
||||
Service string
|
||||
Line string
|
||||
// ContainerEventExit only
|
||||
Source string
|
||||
ID string
|
||||
Service string
|
||||
Line string
|
||||
// ExitCode is only set on ContainerEventExited events
|
||||
ExitCode int
|
||||
Restarting bool
|
||||
}
|
||||
@ -667,17 +685,19 @@ const (
|
||||
ContainerEventLog = iota
|
||||
// ContainerEventErr is a ContainerEvent of type log on stderr. Line is set
|
||||
ContainerEventErr
|
||||
// ContainerEventAttach is a ContainerEvent of type attach. First event sent about a container
|
||||
ContainerEventAttach
|
||||
// ContainerEventStarted let consumer know a container has been started
|
||||
ContainerEventStarted
|
||||
// ContainerEventRestarted let consumer know a container has been restarted
|
||||
ContainerEventRestarted
|
||||
// ContainerEventStopped is a ContainerEvent of type stopped.
|
||||
ContainerEventStopped
|
||||
// ContainerEventCreated let consumer know a new container has been created
|
||||
ContainerEventCreated
|
||||
// ContainerEventRecreated let consumer know container stopped but his being replaced
|
||||
ContainerEventRecreated
|
||||
// ContainerEventExit is a ContainerEvent of type exit. ExitCode is set
|
||||
ContainerEventExit
|
||||
// ContainerEventExited is a ContainerEvent of type exit. ExitCode is set
|
||||
ContainerEventExited
|
||||
// UserCancel user cancelled compose up, we are stopping containers
|
||||
UserCancel
|
||||
// HookEventLog is a ContainerEvent of type log on stdout by service hook
|
||||
HookEventLog
|
||||
)
|
||||
|
||||
|
@ -130,7 +130,7 @@ func (d *DryRunClient) ContainerInspect(ctx context.Context, container string) (
|
||||
ID: id,
|
||||
Name: container,
|
||||
State: &containerType.State{
|
||||
Status: "running", // needed for --wait option
|
||||
Status: containerType.StateRunning, // needed for --wait option
|
||||
Health: &containerType.Health{
|
||||
Status: containerType.Healthy, // needed for healthcheck control
|
||||
},
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
)
|
||||
|
||||
// Streams defines the standard streams (stdin, stdout, stderr) used by the CLI.
|
||||
type Streams interface {
|
||||
Out() *streams.Out
|
||||
Err() *streams.Out
|
||||
|
@ -17,8 +17,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
"github.com/docker/compose/v2/internal"
|
||||
@ -65,9 +63,6 @@ var ComposeVersion string
|
||||
func init() {
|
||||
v, err := version.NewVersion(internal.Version)
|
||||
if err == nil {
|
||||
segments := v.Segments()
|
||||
if len(segments) > 2 {
|
||||
ComposeVersion = fmt.Sprintf("%d.%d.%d", segments[0], segments[1], segments[2])
|
||||
}
|
||||
ComposeVersion = v.Core().String()
|
||||
}
|
||||
}
|
||||
|
35
pkg/api/labels_test.go
Normal file
35
pkg/api/labels_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/compose/v2/internal"
|
||||
"github.com/hashicorp/go-version"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestComposeVersionInitialization(t *testing.T) {
|
||||
v, err := version.NewVersion(internal.Version)
|
||||
if err != nil {
|
||||
assert.Equal(t, "", ComposeVersion, "ComposeVersion should be empty for a non-semver internal version (e.g. 'devel')")
|
||||
} else {
|
||||
expected := v.Core().String()
|
||||
assert.Equal(t, expected, ComposeVersion, "ComposeVersion should be the core of internal.Version")
|
||||
}
|
||||
}
|
@ -23,10 +23,11 @@ import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli/command"
|
||||
cli "github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@ -112,15 +113,20 @@ func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, o
|
||||
return err
|
||||
}
|
||||
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
|
||||
containerConfig := &container.Config{
|
||||
Image: transformation,
|
||||
Env: []string{"LICENSE_AGREEMENT=true"},
|
||||
User: usr.Uid,
|
||||
}, &container.HostConfig{
|
||||
}
|
||||
// On POSIX systems, this is a decimal number representing the uid.
|
||||
// On Windows, this is a security identifier (SID) in a string format and the engine isn't able to manage it
|
||||
if runtime.GOOS != "windows" {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containerConfig.User = usr.Uid
|
||||
}
|
||||
created, err := dockerCli.Client().ContainerCreate(ctx, containerConfig, &container.HostConfig{
|
||||
AutoRemove: true,
|
||||
Binds: binds,
|
||||
}, &network.NetworkingConfig{}, nil, "")
|
||||
@ -198,7 +204,7 @@ func loadFileObject(conf types.FileObjectConfig) (types.FileObjectConfig, error)
|
||||
|
||||
func inspectWithPull(ctx context.Context, dockerCli command.Cli, imageName string) (image.InspectResponse, error) {
|
||||
inspect, err := dockerCli.Client().ImageInspect(ctx, imageName)
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
var stream io.ReadCloser
|
||||
stream, err = dockerCli.Client().ImagePull(ctx, imageName, image.PullOptions{})
|
||||
if err != nil {
|
||||
|
@ -33,6 +33,8 @@ import (
|
||||
const (
|
||||
TransformerLabel = "com.docker.compose.bridge"
|
||||
DefaultTransformerImage = "docker/compose-bridge-kubernetes"
|
||||
|
||||
templatesPath = "/templates"
|
||||
)
|
||||
|
||||
type CreateTransformerOptions struct {
|
||||
@ -73,7 +75,7 @@ func CreateTransformer(ctx context.Context, dockerCli command.Cli, options Creat
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, "/templates")
|
||||
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, templatesPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -82,7 +84,7 @@ func CreateTransformer(ctx context.Context, dockerCli command.Cli, options Creat
|
||||
}()
|
||||
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: "/templates",
|
||||
Path: templatesPath,
|
||||
Exists: true,
|
||||
IsDir: stat.Mode.IsDir(),
|
||||
}
|
||||
|
91
pkg/compose/apiSocket.go
Normal file
91
pkg/compose/apiSocket.go
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
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
|
||||
}
|
@ -61,41 +61,37 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis
|
||||
}
|
||||
|
||||
func (s *composeService) attachContainer(ctx context.Context, container containerType.Summary, listener api.ContainerEventListener) error {
|
||||
serviceName := container.Labels[api.ServiceLabel]
|
||||
containerName := getContainerNameWithoutProject(container)
|
||||
service := container.Labels[api.ServiceLabel]
|
||||
name := getContainerNameWithoutProject(container)
|
||||
return s.doAttachContainer(ctx, service, container.ID, name, listener)
|
||||
}
|
||||
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: containerName,
|
||||
ID: container.ID,
|
||||
Service: serviceName,
|
||||
})
|
||||
|
||||
wOut := utils.GetWriter(func(line string) {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventLog,
|
||||
Container: containerName,
|
||||
ID: container.ID,
|
||||
Service: serviceName,
|
||||
Line: line,
|
||||
})
|
||||
})
|
||||
wErr := utils.GetWriter(func(line string) {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventErr,
|
||||
Container: containerName,
|
||||
ID: container.ID,
|
||||
Service: serviceName,
|
||||
Line: line,
|
||||
})
|
||||
})
|
||||
|
||||
inspect, err := s.apiClient().ContainerInspect(ctx, container.ID)
|
||||
func (s *composeService) doAttachContainer(ctx context.Context, service, id, name string, listener api.ContainerEventListener) error {
|
||||
inspect, err := s.apiClient().ContainerInspect(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _, err = s.attachContainerStreams(ctx, container.ID, inspect.Config.Tty, nil, wOut, wErr)
|
||||
wOut := utils.GetWriter(func(line string) {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventLog,
|
||||
Source: name,
|
||||
ID: id,
|
||||
Service: service,
|
||||
Line: line,
|
||||
})
|
||||
})
|
||||
wErr := utils.GetWriter(func(line string) {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventErr,
|
||||
Source: name,
|
||||
ID: id,
|
||||
Service: service,
|
||||
Line: line,
|
||||
})
|
||||
})
|
||||
|
||||
_, _, err = s.attachContainerStreams(ctx, id, inspect.Config.Tty, nil, wOut, wErr)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -28,7 +29,6 @@ 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"
|
||||
@ -173,7 +173,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||
if options.Quiet {
|
||||
options.Progress = progress.ModeQuiet
|
||||
}
|
||||
if options.Progress == "" {
|
||||
if options.Progress == progress.ModeAuto {
|
||||
options.Progress = os.Getenv("BUILDKIT_PROGRESS")
|
||||
}
|
||||
w, err = xprogress.NewPrinter(progressCtx, os.Stdout, progressui.DisplayMode(options.Progress),
|
||||
@ -398,6 +398,7 @@ func resolveAndMergeBuildArgs(dockerCli command.Cli, project *types.Project, ser
|
||||
return result
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
|
||||
plats, err := parsePlatforms(service)
|
||||
if err != nil {
|
||||
@ -472,8 +473,19 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
}
|
||||
|
||||
attests := map[string]*string{}
|
||||
if !options.Provenance {
|
||||
attests["provenance"] = nil
|
||||
if options.Attestations {
|
||||
if service.Build.Provenance != "" {
|
||||
attests["provenance"] = attestation(service.Build.Provenance, "provenance")
|
||||
}
|
||||
if service.Build.SBOM != "" {
|
||||
attests["sbom"] = attestation(service.Build.SBOM, "sbom")
|
||||
}
|
||||
}
|
||||
if options.Provenance != "" {
|
||||
attests["provenance"] = attestation(options.Provenance, "provenance")
|
||||
}
|
||||
if options.SBOM != "" {
|
||||
attests["sbom"] = attestation(options.SBOM, "sbom")
|
||||
}
|
||||
|
||||
return build.Options{
|
||||
@ -483,8 +495,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
|
||||
NamedContexts: toBuildContexts(service, project),
|
||||
},
|
||||
CacheFrom: pb.CreateCaches(cacheFrom.ToPB()),
|
||||
CacheTo: pb.CreateCaches(cacheTo.ToPB()),
|
||||
CacheFrom: build.CreateCaches(cacheFrom),
|
||||
CacheTo: build.CreateCaches(cacheTo),
|
||||
NoCache: service.Build.NoCache,
|
||||
Pull: service.Build.Pull,
|
||||
BuildArgs: flatten(resolveAndMergeBuildArgs(s.dockerCli, project, service, options)),
|
||||
@ -503,6 +515,16 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
}, nil
|
||||
}
|
||||
|
||||
func attestation(attest string, val string) *string {
|
||||
if b, err := strconv.ParseBool(val); err == nil {
|
||||
s := fmt.Sprintf("type=%s,disabled=%t", attest, b)
|
||||
return &s
|
||||
} else {
|
||||
s := fmt.Sprintf("type=%s,%s", attest, val)
|
||||
return &s
|
||||
}
|
||||
}
|
||||
|
||||
func toUlimitOpt(ulimits map[string]*types.UlimitsConfig) *cliopts.UlimitOpt {
|
||||
ref := map[string]*container.Ulimit{}
|
||||
for _, limit := range toUlimits(ulimits) {
|
||||
|
@ -20,9 +20,11 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -32,20 +34,18 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/socket"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/builder/remotecontext/urlutil"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/util/gitutil"
|
||||
gitutil "github.com/moby/buildkit/frontend/dockerfile/dfgitutil"
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@ -59,6 +59,9 @@ func buildWithBake(dockerCli command.Cli) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
if !bake {
|
||||
if ok {
|
||||
logrus.Warnf("COMPOSE_BAKE=false is deprecated, support for internal compose builder will be removed in next release")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@ -73,7 +76,7 @@ func buildWithBake(dockerCli command.Cli) (bool, error) {
|
||||
|
||||
_, err = manager.GetPlugin("buildx", dockerCli, &cobra.Command{})
|
||||
if err != nil {
|
||||
if manager.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
logrus.Warnf("Docker Compose is configured to build using Bake, but buildx isn't installed")
|
||||
return false, nil
|
||||
}
|
||||
@ -116,6 +119,7 @@ type bakeTarget struct {
|
||||
Entitlements []string `json:"entitlements,omitempty"`
|
||||
ExtraHosts map[string]string `json:"extra-hosts,omitempty"`
|
||||
Outputs []string `json:"output,omitempty"`
|
||||
Attest []string `json:"attest,omitempty"`
|
||||
}
|
||||
|
||||
type bakeMetadata map[string]buildStatus
|
||||
@ -128,7 +132,18 @@ type buildStatus struct {
|
||||
func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
|
||||
eg := errgroup.Group{}
|
||||
ch := make(chan *client.SolveStatus)
|
||||
display, err := progressui.NewDisplay(os.Stdout, progressui.DisplayMode(options.Progress))
|
||||
if options.Progress == progress.ModeAuto {
|
||||
options.Progress = os.Getenv("BUILDKIT_PROGRESS")
|
||||
}
|
||||
displayMode := progressui.DisplayMode(options.Progress)
|
||||
out := options.Out
|
||||
if out == nil {
|
||||
if !s.dockerCli.Out().IsTerminal() {
|
||||
displayMode = progressui.PlainMode
|
||||
}
|
||||
out = os.Stdout // should be s.dockerCli.Out(), but NewDisplay require access to the underlying *File
|
||||
}
|
||||
display, err := progressui.NewDisplay(out, displayMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -142,10 +157,11 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
Targets: map[string]bakeTarget{},
|
||||
}
|
||||
var (
|
||||
group bakeGroup
|
||||
privileged bool
|
||||
read []string
|
||||
targets = make(map[string]string, len(serviceToBeBuild)) // service name -> build target
|
||||
group bakeGroup
|
||||
privileged bool
|
||||
read []string
|
||||
expectedImages = make(map[string]string, len(serviceToBeBuild)) // service name -> expected image
|
||||
targets = make(map[string]string, len(serviceToBeBuild)) // service name -> build target
|
||||
)
|
||||
|
||||
// produce a unique ID for service used as bake target
|
||||
@ -165,6 +181,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
continue
|
||||
}
|
||||
build := *service.Build
|
||||
labels := getImageBuildLabels(project, service)
|
||||
|
||||
args := types.Mapping{}
|
||||
for k, v := range resolveAndMergeBuildArgs(s.dockerCli, project, service, options) {
|
||||
@ -192,7 +209,11 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
case len(service.Build.Platforms) > 1:
|
||||
outputs = []string{fmt.Sprintf("type=image,push=%t", push)}
|
||||
default:
|
||||
outputs = []string{fmt.Sprintf("type=docker,load=true,push=%t", push)}
|
||||
if push {
|
||||
outputs = []string{"type=registry"}
|
||||
} else {
|
||||
outputs = []string{"type=docker"}
|
||||
}
|
||||
}
|
||||
|
||||
read = append(read, build.Context)
|
||||
@ -203,6 +224,12 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
}
|
||||
}
|
||||
|
||||
image := api.GetImageNameOrDefault(service, project.Name)
|
||||
expectedImages[serviceName] = image
|
||||
|
||||
pull := service.Build.Pull || options.Pull
|
||||
noCache := service.Build.NoCache || options.NoCache
|
||||
|
||||
target := targets[serviceName]
|
||||
cfg.Targets[target] = bakeTarget{
|
||||
Context: build.Context,
|
||||
@ -210,17 +237,18 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
Dockerfile: dockerFilePath(build.Context, build.Dockerfile),
|
||||
DockerfileInline: strings.ReplaceAll(build.DockerfileInline, "${", "$${"),
|
||||
Args: args,
|
||||
Labels: build.Labels,
|
||||
Tags: append(build.Tags, api.GetImageNameOrDefault(service, project.Name)),
|
||||
Labels: labels,
|
||||
Tags: append(build.Tags, image),
|
||||
|
||||
CacheFrom: build.CacheFrom,
|
||||
// CacheTo: TODO
|
||||
CacheFrom: build.CacheFrom,
|
||||
CacheTo: build.CacheTo,
|
||||
NetworkMode: build.Network,
|
||||
Platforms: build.Platforms,
|
||||
Target: build.Target,
|
||||
Secrets: toBakeSecrets(project, build.Secrets),
|
||||
SSH: toBakeSSH(append(build.SSH, options.SSHs...)),
|
||||
Pull: options.Pull,
|
||||
NoCache: options.NoCache,
|
||||
Pull: pull,
|
||||
NoCache: noCache,
|
||||
ShmSize: build.ShmSize,
|
||||
Ulimits: toBakeUlimits(build.Ulimits),
|
||||
Entitlements: entitlements,
|
||||
@ -228,6 +256,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
|
||||
Outputs: outputs,
|
||||
Call: call,
|
||||
Attest: toBakeAttest(build),
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,6 +310,12 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
args = append(args, "--allow", "security.insecure")
|
||||
}
|
||||
}
|
||||
if options.SBOM != "" {
|
||||
args = append(args, "--sbom="+options.SBOM)
|
||||
}
|
||||
if options.Provenance != "" {
|
||||
args = append(args, "--provenance="+options.Provenance)
|
||||
}
|
||||
|
||||
if options.Builder != "" {
|
||||
args = append(args, "--builder", options.Builder)
|
||||
@ -291,23 +326,15 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
|
||||
logrus.Debugf("Executing bake with args: %v", args)
|
||||
|
||||
cmd := exec.CommandContext(ctx, buildx.Path, args...)
|
||||
// Remove DOCKER_CLI_PLUGIN... variable so buildx can detect it run standalone
|
||||
cmd.Env = filter(os.Environ(), manager.ReexecEnvvar)
|
||||
|
||||
// Use docker/cli mechanism to propagate termination signal to child process
|
||||
server, err := socket.NewPluginServer(nil)
|
||||
if err == nil {
|
||||
defer server.Close() //nolint:errcheck
|
||||
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
|
||||
if s.dryRun {
|
||||
return dryRunBake(ctx, cfg), nil
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, buildx.Path, args...)
|
||||
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
|
||||
|
||||
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
|
||||
carrier := propagation.MapCarrier{}
|
||||
otel.GetTextMapPropagator().Inject(ctx, &carrier)
|
||||
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
|
||||
err = s.prepareShellOut(ctx, project.Environment, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd.Stdout = s.stdout()
|
||||
cmd.Stdin = bytes.NewBuffer(b)
|
||||
@ -317,16 +344,22 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
}
|
||||
|
||||
var errMessage []string
|
||||
scanner := bufio.NewScanner(pipe)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
reader := bufio.NewReader(pipe)
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eg.Go(cmd.Wait)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
for {
|
||||
line, readErr := reader.ReadString('\n')
|
||||
if readErr != nil {
|
||||
if readErr == io.EOF {
|
||||
break
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to execute bake: %w", readErr)
|
||||
}
|
||||
}
|
||||
decoder := json.NewDecoder(strings.NewReader(line))
|
||||
var status client.SolveStatus
|
||||
err := decoder.Decode(&status)
|
||||
@ -364,13 +397,14 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
cw := progress.ContextWriter(ctx)
|
||||
results := map[string]string{}
|
||||
for name := range serviceToBeBuild {
|
||||
image := expectedImages[name]
|
||||
target := targets[name]
|
||||
built, ok := md[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("build result not found in Bake metadata for service %s", name)
|
||||
}
|
||||
results[name] = built.Digest
|
||||
cw.Event(progress.BuiltEvent(name))
|
||||
results[image] = built.Digest
|
||||
cw.Event(progress.BuiltEvent(image))
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
@ -432,20 +466,28 @@ func toBakeSecrets(project *types.Project, secrets []types.ServiceSecretConfig)
|
||||
return s
|
||||
}
|
||||
|
||||
func filter(environ []string, variable string) []string {
|
||||
prefix := variable + "="
|
||||
filtered := make([]string, 0, len(environ))
|
||||
for _, val := range environ {
|
||||
if !strings.HasPrefix(val, prefix) {
|
||||
filtered = append(filtered, val)
|
||||
func toBakeAttest(build types.BuildConfig) []string {
|
||||
var attests []string
|
||||
|
||||
// Handle per-service provenance configuration (only from build config, not global options)
|
||||
if build.Provenance != "" {
|
||||
if build.Provenance == "true" {
|
||||
attests = append(attests, "type=provenance")
|
||||
} else if build.Provenance != "false" {
|
||||
attests = append(attests, fmt.Sprintf("type=provenance,%s", build.Provenance))
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func replace(environ []string, variable, value string) []string {
|
||||
filtered := filter(environ, variable)
|
||||
return append(filtered, fmt.Sprintf("%s=%s", variable, value))
|
||||
// Handle per-service SBOM configuration (only from build config, not global options)
|
||||
if build.SBOM != "" {
|
||||
if build.SBOM == "true" {
|
||||
attests = append(attests, "type=sbom")
|
||||
} else if build.SBOM != "false" {
|
||||
attests = append(attests, fmt.Sprintf("type=sbom,%s", build.SBOM))
|
||||
}
|
||||
}
|
||||
|
||||
return attests
|
||||
}
|
||||
|
||||
func dockerFilePath(ctxName string, dockerfile string) string {
|
||||
@ -458,9 +500,37 @@ func dockerFilePath(ctxName string, dockerfile string) string {
|
||||
if !filepath.IsAbs(dockerfile) {
|
||||
dockerfile = filepath.Join(ctxName, dockerfile)
|
||||
}
|
||||
symlinks, err := filepath.EvalSymlinks(dockerfile)
|
||||
dir := filepath.Dir(dockerfile)
|
||||
symlinks, err := filepath.EvalSymlinks(dir)
|
||||
if err == nil {
|
||||
return symlinks
|
||||
return filepath.Join(symlinks, filepath.Base(dockerfile))
|
||||
}
|
||||
return dockerfile
|
||||
}
|
||||
|
||||
func dryRunBake(ctx context.Context, cfg bakeConfig) map[string]string {
|
||||
w := progress.ContextWriter(ctx)
|
||||
bakeResponse := map[string]string{}
|
||||
for name, target := range cfg.Targets {
|
||||
dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name)))
|
||||
displayDryRunBuildEvent(w, name, dryRunUUID, target.Tags[0])
|
||||
bakeResponse[name] = dryRunUUID
|
||||
}
|
||||
for name := range bakeResponse {
|
||||
w.Event(progress.BuiltEvent(name))
|
||||
}
|
||||
return bakeResponse
|
||||
}
|
||||
|
||||
func displayDryRunBuildEvent(w progress.Writer, name string, dryRunUUID, tag string) {
|
||||
w.Event(progress.Event{
|
||||
ID: name + " ==>",
|
||||
Status: progress.Done,
|
||||
Text: fmt.Sprintf("==> writing image %s", dryRunUUID),
|
||||
})
|
||||
w.Event(progress.Event{
|
||||
ID: name + " ==> ==>",
|
||||
Status: progress.Done,
|
||||
Text: fmt.Sprintf(`naming to %s`, tag),
|
||||
})
|
||||
}
|
||||
|
@ -70,16 +70,7 @@ func (s composeService) dryRunBuildResponse(ctx context.Context, name string, op
|
||||
w := progress.ContextWriter(ctx)
|
||||
buildResponse := map[string]*client.SolveResponse{}
|
||||
dryRunUUID := fmt.Sprintf("dryRun-%x", sha1.Sum([]byte(name)))
|
||||
w.Event(progress.Event{
|
||||
ID: "==>",
|
||||
Status: progress.Done,
|
||||
Text: fmt.Sprintf("==> writing image %s", dryRunUUID),
|
||||
})
|
||||
w.Event(progress.Event{
|
||||
ID: "==> ==>",
|
||||
Status: progress.Done,
|
||||
Text: fmt.Sprintf(`naming to %s`, options.Tags[0]),
|
||||
})
|
||||
displayDryRunBuildEvent(w, name, dryRunUUID, options.Tags[0])
|
||||
buildResponse[name] = &client.SolveResponse{ExporterResponse: map[string]string{
|
||||
"containerimage.digest": dryRunUUID,
|
||||
}}
|
||||
|
@ -128,12 +128,6 @@ func isService(services ...string) containerPredicate {
|
||||
}
|
||||
}
|
||||
|
||||
func isRunning() containerPredicate {
|
||||
return func(c container.Summary) bool {
|
||||
return c.State == "running"
|
||||
}
|
||||
}
|
||||
|
||||
// isOrphaned is a predicate to select containers without a matching service definition in compose project
|
||||
func isOrphaned(project *types.Project) containerPredicate {
|
||||
services := append(project.ServiceNames(), project.DisabledServiceNames()...)
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
@ -236,7 +237,7 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
|
||||
err := c.service.stop(ctx, project.Name, api.StopOptions{
|
||||
Services: dependents,
|
||||
Project: project,
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -563,6 +564,9 @@ func shouldWaitForDependency(serviceName string, dependencyConfig types.ServiceD
|
||||
} else if service.GetScale() == 0 {
|
||||
// don't wait for the dependency which configured to have 0 containers running
|
||||
return false, nil
|
||||
} else if service.Provider != nil {
|
||||
// don't wait for provider services
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@ -632,13 +636,18 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
|
||||
if inherit {
|
||||
inherited = &replaced
|
||||
}
|
||||
|
||||
replacedContainerName := service.ContainerName
|
||||
if replacedContainerName == "" {
|
||||
replacedContainerName = service.Name + api.Separator + strconv.Itoa(number)
|
||||
}
|
||||
name := getContainerName(project.Name, service, number)
|
||||
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
|
||||
opts := createOptions{
|
||||
AutoRemove: false,
|
||||
AttachStdin: false,
|
||||
UseNetworkAliases: true,
|
||||
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID),
|
||||
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replacedContainerName),
|
||||
}
|
||||
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts, w)
|
||||
if err != nil {
|
||||
@ -656,7 +665,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
|
||||
return created, err
|
||||
}
|
||||
|
||||
err = s.apiClient().ContainerRename(ctx, created.ID, name)
|
||||
err = s.apiClient().ContainerRename(ctx, tmpName, name)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
@ -756,14 +765,7 @@ func (s *composeService) createMobyContainer(ctx context.Context,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = s.injectSecrets(ctx, project, service, created.ID)
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
|
||||
err = s.injectConfigs(ctx, project, service, created.ID)
|
||||
return created, err
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// getLinks mimics V1 compose/service.py::Service::_get_links()
|
||||
@ -826,25 +828,25 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
|
||||
|
||||
func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) {
|
||||
for _, c := range containers {
|
||||
container, err := s.apiClient().ContainerInspect(ctx, c.ID)
|
||||
ctr, err := s.apiClient().ContainerInspect(ctx, c.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
name := container.Name[1:]
|
||||
name := ctr.Name[1:]
|
||||
|
||||
if container.State.Status == "exited" {
|
||||
return false, fmt.Errorf("container %s exited (%d)", name, container.State.ExitCode)
|
||||
if ctr.State.Status == containerType.StateExited {
|
||||
return false, fmt.Errorf("container %s exited (%d)", name, ctr.State.ExitCode)
|
||||
}
|
||||
|
||||
if container.Config.Healthcheck == nil && fallbackRunning {
|
||||
if ctr.Config.Healthcheck == nil && fallbackRunning {
|
||||
// Container does not define a health check, but we can fall back to "running" state
|
||||
return container.State != nil && container.State.Status == "running", nil
|
||||
return ctr.State != nil && ctr.State.Status == containerType.StateRunning, nil
|
||||
}
|
||||
|
||||
if container.State == nil || container.State.Health == nil {
|
||||
if ctr.State == nil || ctr.State.Health == nil {
|
||||
return false, fmt.Errorf("container %s has no healthcheck configured", name)
|
||||
}
|
||||
switch container.State.Health.Status {
|
||||
switch ctr.State.Health.Status {
|
||||
case containerType.Healthy:
|
||||
// Continue by checking the next container.
|
||||
case containerType.Unhealthy:
|
||||
@ -852,7 +854,7 @@ func (s *composeService) isServiceHealthy(ctx context.Context, containers Contai
|
||||
case containerType.Starting:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("container %s had unexpected health status %q", name, container.State.Health.Status)
|
||||
return false, fmt.Errorf("container %s had unexpected health status %q", name, ctr.State.Health.Status)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
@ -860,12 +862,12 @@ func (s *composeService) isServiceHealthy(ctx context.Context, containers Contai
|
||||
|
||||
func (s *composeService) isServiceCompleted(ctx context.Context, containers Containers) (bool, int, error) {
|
||||
for _, c := range containers {
|
||||
container, err := s.apiClient().ContainerInspect(ctx, c.ID)
|
||||
ctr, err := s.apiClient().ContainerInspect(ctx, c.ID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
if container.State != nil && container.State.Status == "exited" {
|
||||
return true, container.State.ExitCode, nil
|
||||
if ctr.State != nil && ctr.State.Status == containerType.StateExited {
|
||||
return true, ctr.State.ExitCode, nil
|
||||
}
|
||||
}
|
||||
return false, 0, nil
|
||||
@ -897,6 +899,17 @@ func (s *composeService) startService(ctx context.Context,
|
||||
if ctr.State == ContainerRunning {
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.injectSecrets(ctx, project, service, ctr.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.injectConfigs(ctx, project, service, ctr.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eventName := getContainerProgressName(ctr)
|
||||
w.Event(progress.StartingEvent(eventName))
|
||||
err = s.apiClient().ContainerStart(ctx, ctr.ID, containerType.StartOptions{})
|
||||
@ -919,9 +932,7 @@ func (s *composeService) startService(ctx context.Context,
|
||||
func mergeLabels(ls ...types.Labels) types.Labels {
|
||||
merged := types.Labels{}
|
||||
for _, l := range ls {
|
||||
for k, v := range l {
|
||||
merged[k] = v
|
||||
}
|
||||
maps.Copy(merged, l)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/moby/go-archive"
|
||||
)
|
||||
|
||||
@ -161,7 +160,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := dstStat.LinkTarget
|
||||
if !system.IsAbs(linkTarget) {
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||
@ -264,7 +263,7 @@ func (s *composeService) copyFromContainer(ctx context.Context, containerID, src
|
||||
// If the destination is a symbolic link, we should follow it.
|
||||
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := srcStat.LinkTarget
|
||||
if !system.IsAbs(linkTarget) {
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||
linkTarget = filepath.Join(srcParent, linkTarget)
|
||||
@ -302,8 +301,20 @@ func (s *composeService) copyFromContainer(ctx context.Context, containerID, src
|
||||
return archive.CopyTo(preArchive, srcInfo, dstPath)
|
||||
}
|
||||
|
||||
// IsAbs is a platform-agnostic wrapper for filepath.IsAbs.
|
||||
//
|
||||
// On Windows, golang filepath.IsAbs does not consider a path \windows\system32
|
||||
// as absolute as it doesn't start with a drive-letter/colon combination. However,
|
||||
// in docker we need to verify things such as WORKDIR /windows/system32 in
|
||||
// a Dockerfile (which gets translated to \windows\system32 when being processed
|
||||
// by the daemon). This SHOULD be treated as absolute from a docker processing
|
||||
// perspective.
|
||||
func isAbs(path string) bool {
|
||||
return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator))
|
||||
}
|
||||
|
||||
func splitCpArg(arg string) (ctr, path string) {
|
||||
if system.IsAbs(arg) {
|
||||
if isAbs(arg) {
|
||||
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
|
||||
return "", arg
|
||||
}
|
||||
|
@ -30,13 +30,12 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/paths"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/docker/api/types/blkiodev"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
volumetypes "github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/go-connections/nat"
|
||||
@ -83,6 +82,11 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ensureModels(ctx, project, options.QuietPull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prepareNetworks(project)
|
||||
|
||||
networks, err := s.ensureNetworks(ctx, project)
|
||||
@ -114,6 +118,13 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
|
||||
"--remove-orphans flag to clean it up.", orphans.names())
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary implementation of use_api_socket until we get actual support inside docker engine
|
||||
project, err = s.useAPISocket(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return newConvergence(options.Services, observedState, networks, volumes, s).apply(ctx, project, options)
|
||||
}
|
||||
|
||||
@ -169,15 +180,12 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
|
||||
return createConfigs{}, err
|
||||
}
|
||||
|
||||
var (
|
||||
runCmd strslice.StrSlice
|
||||
entrypoint strslice.StrSlice
|
||||
)
|
||||
var runCmd, entrypoint []string
|
||||
if service.Command != nil {
|
||||
runCmd = strslice.StrSlice(service.Command)
|
||||
runCmd = service.Command
|
||||
}
|
||||
if service.Entrypoint != nil {
|
||||
entrypoint = strslice.StrSlice(service.Entrypoint)
|
||||
entrypoint = service.Entrypoint
|
||||
}
|
||||
|
||||
var (
|
||||
@ -274,8 +282,8 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
|
||||
Annotations: service.Annotations,
|
||||
Binds: binds,
|
||||
Mounts: mounts,
|
||||
CapAdd: strslice.StrSlice(service.CapAdd),
|
||||
CapDrop: strslice.StrSlice(service.CapDrop),
|
||||
CapAdd: service.CapAdd,
|
||||
CapDrop: service.CapDrop,
|
||||
NetworkMode: networkMode,
|
||||
Init: service.Init,
|
||||
IpcMode: container.IpcMode(service.Ipc),
|
||||
@ -1254,7 +1262,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
|
||||
}
|
||||
|
||||
id, err := s.resolveOrCreateNetwork(ctx, project, name, n)
|
||||
if cerrdefs.IsConflict(err) {
|
||||
if errdefs.IsConflict(err) {
|
||||
// Maybe another execution of `docker compose up|run` created same network
|
||||
// let's retry once
|
||||
return s.resolveOrCreateNetwork(ctx, project, name, n)
|
||||
@ -1420,7 +1428,7 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
|
||||
err := s.stop(ctx, project.Name, api.StopOptions{
|
||||
Services: services,
|
||||
Project: project,
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1489,7 +1497,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
|
||||
sn, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
|
||||
if err == nil {
|
||||
networks = append(networks, sn)
|
||||
} else if !cerrdefs.IsNotFound(err) {
|
||||
} else if !errdefs.IsNotFound(err) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@ -1526,7 +1534,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
|
||||
func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project, assumeYes bool) (string, error) {
|
||||
inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
|
||||
if err != nil {
|
||||
if !cerrdefs.IsNotFound(err) {
|
||||
if !errdefs.IsNotFound(err) {
|
||||
return "", err
|
||||
}
|
||||
if volume.External {
|
||||
@ -1591,7 +1599,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
|
||||
err := s.stop(ctx, project.Name, api.StopOptions{
|
||||
Services: services,
|
||||
Project: project,
|
||||
})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1615,7 +1623,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
|
||||
}
|
||||
|
||||
func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {
|
||||
eventName := fmt.Sprintf("Volume %q", volume.Name)
|
||||
eventName := fmt.Sprintf("Volume %s", volume.Name)
|
||||
w := progress.ContextWriter(ctx)
|
||||
w.Event(progress.CreatingEvent(eventName))
|
||||
hash, err := VolumeHash(volume)
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
@ -219,7 +219,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
continue
|
||||
}
|
||||
nw, err := s.apiClient().NetworkInspect(ctx, net.ID, network.InspectOptions{})
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
@ -233,7 +233,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
}
|
||||
|
||||
if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
w.Event(progress.ErrorEvent(eventName))
|
||||
@ -261,11 +261,11 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre
|
||||
w.Event(progress.NewEvent(id, progress.Done, "Removed"))
|
||||
return nil
|
||||
}
|
||||
if cerrdefs.IsConflict(err) {
|
||||
if errdefs.IsConflict(err) {
|
||||
w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
|
||||
return nil
|
||||
}
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
@ -276,7 +276,7 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
|
||||
resource := fmt.Sprintf("Volume %s", id)
|
||||
|
||||
_, err := s.apiClient().VolumeInspect(ctx, id)
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
// Already gone
|
||||
return nil
|
||||
}
|
||||
@ -287,25 +287,33 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
|
||||
w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
|
||||
return nil
|
||||
}
|
||||
if cerrdefs.IsConflict(err) {
|
||||
if errdefs.IsConflict(err) {
|
||||
w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
|
||||
return nil
|
||||
}
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, service *types.ServiceConfig, ctr containerType.Summary, timeout *time.Duration) error {
|
||||
func (s *composeService) stopContainer(
|
||||
ctx context.Context, w progress.Writer,
|
||||
service *types.ServiceConfig, ctr containerType.Summary,
|
||||
timeout *time.Duration, listener api.ContainerEventListener,
|
||||
) error {
|
||||
eventName := getContainerProgressName(ctr)
|
||||
w.Event(progress.StoppingEvent(eventName))
|
||||
|
||||
if service != nil {
|
||||
for _, hook := range service.PreStop {
|
||||
err := s.runHook(ctx, ctr, *service, hook, nil)
|
||||
err := s.runHook(ctx, ctr, *service, hook, listener)
|
||||
if err != nil {
|
||||
// Ignore errors indicating that some containers were already stopped or removed.
|
||||
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -321,11 +329,15 @@ func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, serv *types.ServiceConfig, containers []containerType.Summary, timeout *time.Duration) error {
|
||||
func (s *composeService) stopContainers(
|
||||
ctx context.Context, w progress.Writer,
|
||||
serv *types.ServiceConfig, containers []containerType.Summary,
|
||||
timeout *time.Duration, listener api.ContainerEventListener,
|
||||
) error {
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, ctr := range containers {
|
||||
eg.Go(func() error {
|
||||
return s.stopContainer(ctx, w, serv, ctr, timeout)
|
||||
return s.stopContainer(ctx, w, serv, ctr, timeout, listener)
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
@ -344,8 +356,8 @@ func (s *composeService) removeContainers(ctx context.Context, containers []cont
|
||||
func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr containerType.Summary, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
|
||||
w := progress.ContextWriter(ctx)
|
||||
eventName := getContainerProgressName(ctr)
|
||||
err := s.stopContainer(ctx, w, service, ctr, timeout)
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
err := s.stopContainer(ctx, w, service, ctr, timeout, nil)
|
||||
if errdefs.IsNotFound(err) {
|
||||
w.Event(progress.RemovedEvent(eventName))
|
||||
return nil
|
||||
}
|
||||
@ -357,7 +369,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
|
||||
Force: true,
|
||||
RemoveVolumes: volumes,
|
||||
})
|
||||
if err != nil && !cerrdefs.IsNotFound(err) && !cerrdefs.IsConflict(err) {
|
||||
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
|
||||
w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
|
||||
return err
|
||||
}
|
||||
|
@ -24,13 +24,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
@ -326,7 +326,7 @@ func TestDownRemoveImages(t *testing.T) {
|
||||
if exists {
|
||||
resp.RepoTags = []string{img}
|
||||
} else {
|
||||
err = errdefs.NotFound(fmt.Errorf("test specified that image %q should not exist", img))
|
||||
err = errdefs.ErrNotFound.WithMessage(fmt.Sprintf("test specified that image %q should not exist", img))
|
||||
}
|
||||
|
||||
api.EXPECT().ImageInspect(gomock.Any(), img).
|
||||
|
@ -32,6 +32,8 @@ func (s *composeService) Events(ctx context.Context, projectName string, options
|
||||
projectName = strings.ToLower(projectName)
|
||||
evts, errors := s.apiClient().Events(ctx, events.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(projectName)),
|
||||
Since: options.Since,
|
||||
Until: options.Until,
|
||||
})
|
||||
for {
|
||||
select {
|
||||
|
@ -52,7 +52,7 @@ func (s *composeService) Exec(ctx context.Context, projectName string, options a
|
||||
err = container.RunExec(ctx, s.dockerCli, target.ID, exec)
|
||||
var sterr cli.StatusError
|
||||
if errors.As(err, &sterr) {
|
||||
return sterr.StatusCode, nil
|
||||
return sterr.StatusCode, err
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
@ -32,11 +32,11 @@ import (
|
||||
func (s composeService) runHook(ctx context.Context, ctr container.Summary, service types.ServiceConfig, hook types.ServiceHook, listener api.ContainerEventListener) error {
|
||||
wOut := utils.GetWriter(func(line string) {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.HookEventLog,
|
||||
Container: getContainerNameWithoutProject(ctr) + " ->",
|
||||
ID: ctr.ID,
|
||||
Service: service.Name,
|
||||
Line: line,
|
||||
Type: api.HookEventLog,
|
||||
Source: getContainerNameWithoutProject(ctr) + " ->",
|
||||
ID: ctr.ID,
|
||||
Service: service.Name,
|
||||
Line: line,
|
||||
})
|
||||
})
|
||||
defer wOut.Close() //nolint:errcheck
|
||||
@ -48,7 +48,6 @@ func (s composeService) runHook(ctx context.Context, ctr container.Summary, serv
|
||||
Env: ToMobyEnv(hook.Environment),
|
||||
WorkingDir: hook.WorkingDir,
|
||||
Cmd: hook.Command,
|
||||
Detach: detached,
|
||||
AttachStdout: !detached,
|
||||
AttachStderr: !detached,
|
||||
})
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
@ -204,7 +204,7 @@ func (p *ImagePruner) filterImagesByExistence(ctx context.Context, imageNames []
|
||||
for _, img := range imageNames {
|
||||
eg.Go(func() error {
|
||||
_, err := p.client.ImageInspect(ctx, img)
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
// err on the side of caution: only skip if we successfully
|
||||
// queried the API and got back a definitive "not exists"
|
||||
return nil
|
||||
|
@ -22,8 +22,9 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@ -90,6 +91,11 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
|
||||
}
|
||||
}
|
||||
|
||||
created, err := time.Parse(time.RFC3339Nano, image.Created)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
summary[getCanonicalContainerName(c)] = api.ImageSummary{
|
||||
@ -103,6 +109,7 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
|
||||
Variant: image.Variant,
|
||||
},
|
||||
Size: image.Size,
|
||||
Created: created,
|
||||
LastTagTime: image.Metadata.LastTagTime,
|
||||
}
|
||||
return nil
|
||||
@ -121,7 +128,7 @@ func (s *composeService) getImageSummaries(ctx context.Context, repoTags []strin
|
||||
eg.Go(func() error {
|
||||
inspect, err := s.apiClient().ImageInspect(ctx, repoTag)
|
||||
if err != nil {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unable to get image '%s': %w", repoTag, err)
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@ -44,8 +45,12 @@ func TestImages(t *testing.T) {
|
||||
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)))
|
||||
listOpts := container.ListOptions{All: true, Filters: args}
|
||||
api.EXPECT().ServerVersion(gomock.Any()).Return(types.Version{APIVersion: "1.96"}, nil).AnyTimes()
|
||||
image1 := imageInspect("image1", "foo:1", 12345)
|
||||
image2 := imageInspect("image2", "bar:2", 67890)
|
||||
timeStr1 := "2025-06-06T06:06:06.000000000Z"
|
||||
created1, _ := time.Parse(time.RFC3339Nano, timeStr1)
|
||||
timeStr2 := "2025-03-03T03:03:03.000000000Z"
|
||||
created2, _ := time.Parse(time.RFC3339Nano, timeStr2)
|
||||
image1 := imageInspect("image1", "foo:1", 12345, timeStr1)
|
||||
image2 := imageInspect("image2", "bar:2", 67890, timeStr2)
|
||||
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(image1, nil).MaxTimes(2)
|
||||
api.EXPECT().ImageInspect(anyCancellableContext(), "bar:2").Return(image2, nil)
|
||||
c1 := containerDetail("service1", "123", "running", "foo:1")
|
||||
@ -62,32 +67,36 @@ func TestImages(t *testing.T) {
|
||||
Repository: "foo",
|
||||
Tag: "1",
|
||||
Size: 12345,
|
||||
Created: created1,
|
||||
},
|
||||
"456": {
|
||||
ID: "image2",
|
||||
Repository: "bar",
|
||||
Tag: "2",
|
||||
Size: 67890,
|
||||
Created: created2,
|
||||
},
|
||||
"789": {
|
||||
ID: "image1",
|
||||
Repository: "foo",
|
||||
Tag: "1",
|
||||
Size: 12345,
|
||||
Created: created1,
|
||||
},
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, images, expected)
|
||||
}
|
||||
|
||||
func imageInspect(id string, imageReference string, size int64) image.InspectResponse {
|
||||
func imageInspect(id string, imageReference string, size int64, created string) image.InspectResponse {
|
||||
return image.InspectResponse{
|
||||
ID: id,
|
||||
RepoTags: []string{
|
||||
"someRepo:someTag",
|
||||
imageReference,
|
||||
},
|
||||
Size: size,
|
||||
Size: size,
|
||||
Created: created,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,12 +18,10 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@ -63,9 +61,8 @@ func (s *composeService) Logs(
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, ctr := range containers {
|
||||
eg.Go(func() error {
|
||||
err := s.logContainers(ctx, consumer, ctr, options)
|
||||
var notImplErr errdefs.ErrNotImplemented
|
||||
if errors.As(err, ¬ImplErr) {
|
||||
err := s.logContainer(ctx, consumer, ctr, options)
|
||||
if errdefs.IsNotImplemented(err) {
|
||||
logrus.Warnf("Can't retrieve logs for %q: %s", getCanonicalContainerName(ctr), err.Error())
|
||||
return nil
|
||||
}
|
||||
@ -74,70 +71,57 @@ func (s *composeService) Logs(
|
||||
}
|
||||
|
||||
if options.Follow {
|
||||
containers = containers.filter(isRunning())
|
||||
printer := newLogPrinter(consumer)
|
||||
eg.Go(func() error {
|
||||
_, err := printer.Run(api.CascadeIgnore, "", nil)
|
||||
return err
|
||||
})
|
||||
|
||||
for _, c := range containers {
|
||||
printer.HandleEvent(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: getContainerNameWithoutProject(c),
|
||||
ID: c.ID,
|
||||
Service: c.Labels[api.ServiceLabel],
|
||||
})
|
||||
monitor := newMonitor(s.apiClient(), projectName)
|
||||
if len(options.Services) > 0 {
|
||||
monitor.withServices(options.Services)
|
||||
} else if options.Project != nil {
|
||||
monitor.withServices(options.Project.ServiceNames())
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
err := s.watchContainers(ctx, projectName, options.Services, nil, printer.HandleEvent, containers, func(c container.Summary, t time.Time) error {
|
||||
printer.HandleEvent(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: getContainerNameWithoutProject(c),
|
||||
ID: c.ID,
|
||||
Service: c.Labels[api.ServiceLabel],
|
||||
})
|
||||
monitor.withListener(printer.HandleEvent)
|
||||
monitor.withListener(func(event api.ContainerEvent) {
|
||||
if event.Type == api.ContainerEventStarted {
|
||||
eg.Go(func() error {
|
||||
err := s.logContainers(ctx, consumer, c, api.LogOptions{
|
||||
ctr, err := s.apiClient().ContainerInspect(ctx, event.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.doLogContainer(ctx, consumer, event.Source, ctr, api.LogOptions{
|
||||
Follow: options.Follow,
|
||||
Since: t.Format(time.RFC3339Nano),
|
||||
Since: ctr.State.StartedAt,
|
||||
Until: options.Until,
|
||||
Tail: options.Tail,
|
||||
Timestamps: options.Timestamps,
|
||||
})
|
||||
var notImplErr errdefs.ErrNotImplemented
|
||||
if errors.As(err, ¬ImplErr) {
|
||||
if errdefs.IsNotImplemented(err) {
|
||||
// ignore
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
return nil
|
||||
}, func(c container.Summary, t time.Time) error {
|
||||
printer.HandleEvent(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: "", // actual name will be set by start event
|
||||
ID: c.ID,
|
||||
Service: c.Labels[api.ServiceLabel],
|
||||
})
|
||||
return nil
|
||||
})
|
||||
printer.Stop()
|
||||
return err
|
||||
}
|
||||
})
|
||||
eg.Go(func() error {
|
||||
return monitor.Start(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) logContainers(ctx context.Context, consumer api.LogConsumer, c container.Summary, options api.LogOptions) error {
|
||||
cnt, err := s.apiClient().ContainerInspect(ctx, c.ID)
|
||||
func (s *composeService) logContainer(ctx context.Context, consumer api.LogConsumer, c container.Summary, options api.LogOptions) error {
|
||||
ctr, err := s.apiClient().ContainerInspect(ctx, c.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := getContainerNameWithoutProject(c)
|
||||
return s.doLogContainer(ctx, consumer, name, ctr, options)
|
||||
}
|
||||
|
||||
r, err := s.apiClient().ContainerLogs(ctx, cnt.ID, container.LogsOptions{
|
||||
func (s *composeService) doLogContainer(ctx context.Context, consumer api.LogConsumer, name string, ctr container.InspectResponse, options api.LogOptions) error {
|
||||
r, err := s.apiClient().ContainerLogs(ctx, ctr.ID, container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: options.Follow,
|
||||
@ -151,11 +135,10 @@ func (s *composeService) logContainers(ctx context.Context, consumer api.LogCons
|
||||
}
|
||||
defer r.Close() //nolint:errcheck
|
||||
|
||||
name := getContainerNameWithoutProject(c)
|
||||
w := utils.GetWriter(func(line string) {
|
||||
consumer.Log(name, line)
|
||||
})
|
||||
if cnt.Config.Tty {
|
||||
if ctr.Config.Tty {
|
||||
_, err = io.Copy(w, r)
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(w, w, r)
|
||||
|
@ -189,8 +189,6 @@ func (l *testLogConsumer) Err(containerName, message string) {
|
||||
|
||||
func (l *testLogConsumer) Status(containerName, msg string) {}
|
||||
|
||||
func (l *testLogConsumer) Register(containerName string) {}
|
||||
|
||||
func (l *testLogConsumer) LogsForContainer(containerName string) []string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
254
pkg/compose/model.go
Normal file
254
pkg/compose/model.go
Normal file
@ -0,0 +1,254 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (s *composeService) ensureModels(ctx context.Context, project *types.Project, quietPull bool) error {
|
||||
if len(project.Models) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
api, err := s.newModelAPI(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
availableModels, err := api.ListModels(ctx)
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
return api.SetModelVariables(ctx, project)
|
||||
})
|
||||
|
||||
w := progress.ContextWriter(ctx)
|
||||
for name, config := range project.Models {
|
||||
if config.Name == "" {
|
||||
config.Name = name
|
||||
}
|
||||
eg.Go(func() error {
|
||||
if !slices.Contains(availableModels, config.Model) {
|
||||
err = api.PullModel(ctx, config, quietPull, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return api.ConfigureModel(ctx, config, w)
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
type modelAPI struct {
|
||||
path string
|
||||
env []string
|
||||
prepare func(ctx context.Context, cmd *exec.Cmd) error
|
||||
}
|
||||
|
||||
func (s *composeService) newModelAPI(project *types.Project) (*modelAPI, error) {
|
||||
dockerModel, err := manager.GetPlugin("model", s.dockerCli, &cobra.Command{})
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("'models' support requires Docker Model plugin")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &modelAPI{
|
||||
path: dockerModel.Path,
|
||||
prepare: func(ctx context.Context, cmd *exec.Cmd) error {
|
||||
return s.prepareShellOut(ctx, project.Environment, cmd)
|
||||
},
|
||||
env: project.Environment.Values(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, w progress.Writer) error {
|
||||
w.Event(progress.Event{
|
||||
ID: model.Name,
|
||||
Status: progress.Working,
|
||||
Text: "Pulling",
|
||||
})
|
||||
|
||||
cmd := exec.CommandContext(ctx, m.path, "pull", model.Model)
|
||||
err := m.prepare(ctx, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stream, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stream)
|
||||
for scanner.Scan() {
|
||||
msg := scanner.Text()
|
||||
if msg == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !quietPull {
|
||||
w.Event(progress.Event{
|
||||
ID: model.Name,
|
||||
Status: progress.Working,
|
||||
Text: "Pulling",
|
||||
StatusText: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
w.Event(progress.ErrorMessageEvent(model.Name, err.Error()))
|
||||
}
|
||||
w.Event(progress.Event{
|
||||
ID: model.Name,
|
||||
Status: progress.Working,
|
||||
Text: "Pulled",
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, w progress.Writer) error {
|
||||
w.Event(progress.Event{
|
||||
ID: config.Name,
|
||||
Status: progress.Working,
|
||||
Text: "Configuring",
|
||||
})
|
||||
// configure [--context-size=<n>] MODEL [-- <runtime-flags...>]
|
||||
args := []string{"configure"}
|
||||
if config.ContextSize > 0 {
|
||||
args = append(args, "--context-size", strconv.Itoa(config.ContextSize))
|
||||
}
|
||||
args = append(args, config.Model)
|
||||
if len(config.RuntimeFlags) != 0 {
|
||||
args = append(args, "--")
|
||||
args = append(args, config.RuntimeFlags...)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, m.path, args...)
|
||||
err := m.prepare(ctx, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (m *modelAPI) SetModelVariables(ctx context.Context, project *types.Project) error {
|
||||
cmd := exec.CommandContext(ctx, m.path, "status", "--json")
|
||||
err := m.prepare(ctx, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statusOut, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking docker-model status: %w", err)
|
||||
}
|
||||
type Status struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
}
|
||||
|
||||
var status Status
|
||||
err = json.Unmarshal(statusOut, &status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, service := range project.Services {
|
||||
for ref, modelConfig := range service.Models {
|
||||
model := project.Models[ref]
|
||||
varPrefix := strings.ReplaceAll(strings.ToUpper(ref), "-", "_")
|
||||
var variable string
|
||||
if modelConfig != nil && modelConfig.ModelVariable != "" {
|
||||
variable = modelConfig.ModelVariable
|
||||
} else {
|
||||
variable = varPrefix + "_MODEL"
|
||||
}
|
||||
service.Environment[variable] = &model.Model
|
||||
|
||||
if modelConfig != nil && modelConfig.EndpointVariable != "" {
|
||||
variable = modelConfig.EndpointVariable
|
||||
} else {
|
||||
variable = varPrefix + "_URL"
|
||||
}
|
||||
service.Environment[variable] = &status.Endpoint
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Id string `json:"id"`
|
||||
Tags []string `json:"tags"`
|
||||
Created int `json:"created"`
|
||||
Config struct {
|
||||
Format string `json:"format"`
|
||||
Quantization string `json:"quantization"`
|
||||
Parameters string `json:"parameters"`
|
||||
Architecture string `json:"architecture"`
|
||||
Size string `json:"size"`
|
||||
} `json:"config"`
|
||||
}
|
||||
|
||||
func (m *modelAPI) ListModels(ctx context.Context) ([]string, error) {
|
||||
cmd := exec.CommandContext(ctx, m.path, "ls", "--json")
|
||||
err := m.prepare(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking available models: %w", err)
|
||||
}
|
||||
|
||||
type AvailableModel struct {
|
||||
Id string `json:"id"`
|
||||
Tags []string `json:"tags"`
|
||||
Created int `json:"created"`
|
||||
}
|
||||
|
||||
models := []AvailableModel{}
|
||||
err = json.Unmarshal(output, &models)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling available models: %w", err)
|
||||
}
|
||||
var availableModels []string
|
||||
for _, model := range models {
|
||||
availableModels = append(availableModels, model.Tags...)
|
||||
}
|
||||
return availableModels, nil
|
||||
}
|
218
pkg/compose/monitor.go
Normal file
218
pkg/compose/monitor.go
Normal file
@ -0,0 +1,218 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
type monitor struct {
|
||||
api client.APIClient
|
||||
project string
|
||||
// services tells us which service to consider and those we can ignore, maybe ran by a concurrent compose command
|
||||
services map[string]bool
|
||||
listeners []api.ContainerEventListener
|
||||
}
|
||||
|
||||
func newMonitor(api client.APIClient, project string) *monitor {
|
||||
return &monitor{
|
||||
api: api,
|
||||
project: project,
|
||||
services: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *monitor) withServices(services []string) {
|
||||
for _, name := range services {
|
||||
c.services[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Start runs monitor to detect application events and return after termination
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (c *monitor) Start(ctx context.Context) error {
|
||||
// collect initial application container
|
||||
initialState, err := c.api.ContainerList(ctx, container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
projectFilter(c.project),
|
||||
oneOffFilter(false),
|
||||
hasConfigHashLabel(),
|
||||
),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// containers is the set if container IDs the application is based on
|
||||
containers := utils.Set[string]{}
|
||||
for _, ctr := range initialState {
|
||||
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
|
||||
containers.Add(ctr.ID)
|
||||
}
|
||||
}
|
||||
restarting := utils.Set[string]{}
|
||||
|
||||
evtCh, errCh := c.api.Events(context.Background(), events.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("type", "container"),
|
||||
projectFilter(c.project)),
|
||||
})
|
||||
for {
|
||||
if len(containers) == 0 {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return err
|
||||
case event := <-evtCh:
|
||||
if len(c.services) > 0 && !c.services[event.Actor.Attributes[api.ServiceLabel]] {
|
||||
continue
|
||||
}
|
||||
ctr, err := c.getContainerSummary(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch event.Action {
|
||||
case events.ActionCreate:
|
||||
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
|
||||
containers.Add(ctr.ID)
|
||||
}
|
||||
evtType := api.ContainerEventCreated
|
||||
if _, ok := ctr.Labels[api.ContainerReplaceLabel]; ok {
|
||||
evtType = api.ContainerEventRecreated
|
||||
}
|
||||
for _, listener := range c.listeners {
|
||||
listener(newContainerEvent(event.TimeNano, ctr, evtType))
|
||||
}
|
||||
logrus.Debugf("container %s created", ctr.Name)
|
||||
case events.ActionStart:
|
||||
restarted := restarting.Has(ctr.ID)
|
||||
if restarted {
|
||||
logrus.Debugf("container %s restarted", ctr.Name)
|
||||
for _, listener := range c.listeners {
|
||||
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventStarted, func(e *api.ContainerEvent) {
|
||||
e.Restarting = restarted
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
logrus.Debugf("container %s started", ctr.Name)
|
||||
for _, listener := range c.listeners {
|
||||
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventStarted))
|
||||
}
|
||||
}
|
||||
if len(c.services) == 0 || c.services[ctr.Labels[api.ServiceLabel]] {
|
||||
containers.Add(ctr.ID)
|
||||
}
|
||||
case events.ActionRestart:
|
||||
for _, listener := range c.listeners {
|
||||
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventRestarted))
|
||||
}
|
||||
logrus.Debugf("container %s restarted", ctr.Name)
|
||||
case events.ActionStop:
|
||||
// when a container is in restarting phase, and we stop the application (abort-on-container-exit)
|
||||
// we won't get any additional start+die events, just this stop as a proof container is down
|
||||
logrus.Debugf("container %s stopped", ctr.Name)
|
||||
containers.Remove(ctr.ID)
|
||||
case events.ActionDie:
|
||||
logrus.Debugf("container %s exited with code %d", ctr.Name, ctr.ExitCode)
|
||||
inspect, err := c.api.ContainerInspect(ctx, event.Actor.ID)
|
||||
if errdefs.IsNotFound(err) {
|
||||
// Source is already removed
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if inspect.State != nil && inspect.State.Restarting || inspect.State.Running {
|
||||
// State.Restarting is set by engine when container is configured to restart on exit
|
||||
// on ContainerRestart it doesn't (see https://github.com/moby/moby/issues/45538)
|
||||
// container state still is reported as "running"
|
||||
logrus.Debugf("container %s is restarting", ctr.Name)
|
||||
restarting.Add(ctr.ID)
|
||||
for _, listener := range c.listeners {
|
||||
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventExited, func(e *api.ContainerEvent) {
|
||||
e.Restarting = true
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
for _, listener := range c.listeners {
|
||||
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventExited))
|
||||
}
|
||||
containers.Remove(ctr.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newContainerEvent(timeNano int64, ctr *api.ContainerSummary, eventType int, opts ...func(e *api.ContainerEvent)) api.ContainerEvent {
|
||||
name := ctr.Name
|
||||
defaultName := getDefaultContainerName(ctr.Project, ctr.Labels[api.ServiceLabel], ctr.Labels[api.ContainerNumberLabel])
|
||||
if name == defaultName {
|
||||
// remove project- prefix
|
||||
name = name[len(ctr.Project)+1:]
|
||||
}
|
||||
|
||||
event := api.ContainerEvent{
|
||||
Type: eventType,
|
||||
Container: ctr,
|
||||
Time: timeNano,
|
||||
Source: name,
|
||||
ID: ctr.ID,
|
||||
Service: ctr.Service,
|
||||
ExitCode: ctr.ExitCode,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&event)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
func (c *monitor) getContainerSummary(event events.Message) (*api.ContainerSummary, error) {
|
||||
ctr := &api.ContainerSummary{
|
||||
ID: event.Actor.ID,
|
||||
Name: event.Actor.Attributes["name"],
|
||||
Project: c.project,
|
||||
Service: event.Actor.Attributes[api.ServiceLabel],
|
||||
Labels: event.Actor.Attributes, // More than just labels, but that'c the closest the API gives us
|
||||
}
|
||||
if ec, ok := event.Actor.Attributes["exitCode"]; ok {
|
||||
exitCode, err := strconv.Atoi(ec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctr.ExitCode = exitCode
|
||||
}
|
||||
return ctr, nil
|
||||
}
|
||||
|
||||
func (c *monitor) withListener(listener api.ContainerEventListener) {
|
||||
c.listeners = append(c.listeners, listener)
|
||||
}
|
@ -27,16 +27,15 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/socket"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
)
|
||||
|
||||
type JsonMessage struct {
|
||||
@ -52,6 +51,8 @@ const (
|
||||
providerMetadataDirectory = "compose/providers"
|
||||
)
|
||||
|
||||
var mux sync.Mutex
|
||||
|
||||
func (s *composeService) runPlugin(ctx context.Context, project *types.Project, service types.ServiceConfig, command string) error {
|
||||
provider := *service.Provider
|
||||
|
||||
@ -70,6 +71,8 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
|
||||
return err
|
||||
}
|
||||
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
for name, s := range project.Services {
|
||||
if _, ok := s.DependsOn[service.Name]; ok {
|
||||
prefix := strings.ToUpper(service.Name) + "_"
|
||||
@ -161,14 +164,14 @@ func (s *composeService) getPluginBinaryPath(provider string) (path string, err
|
||||
if err == nil {
|
||||
path = plugin.Path
|
||||
}
|
||||
if manager.IsNotFound(err) {
|
||||
if errdefs.IsNotFound(err) {
|
||||
path, err = exec.LookPath(executable(provider))
|
||||
}
|
||||
return path, err
|
||||
}
|
||||
|
||||
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) (*exec.Cmd, error) {
|
||||
cmdOptionsMetadata := s.getPluginMetadata(path, service.Provider.Type)
|
||||
cmdOptionsMetadata := s.getPluginMetadata(path, service.Provider.Type, project)
|
||||
var currentCommandMetadata CommandMetadata
|
||||
switch command {
|
||||
case "up":
|
||||
@ -176,8 +179,9 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
|
||||
case "down":
|
||||
currentCommandMetadata = cmdOptionsMetadata.Down
|
||||
}
|
||||
commandMetadataIsEmpty := len(currentCommandMetadata.Parameters) == 0
|
||||
|
||||
provider := *service.Provider
|
||||
commandMetadataIsEmpty := cmdOptionsMetadata.IsEmpty()
|
||||
if err := currentCommandMetadata.CheckRequiredParameters(provider); !commandMetadataIsEmpty && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -193,34 +197,21 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
|
||||
args = append(args, service.Name)
|
||||
|
||||
cmd := exec.CommandContext(ctx, path, args...)
|
||||
// exec provider command with same environment Compose is running
|
||||
env := types.NewMapping(os.Environ())
|
||||
// but remove DOCKER_CLI_PLUGIN... variable so plugin can detect it run standalone
|
||||
delete(env, manager.ReexecEnvvar)
|
||||
// and add the explicit environment variables set for service
|
||||
for key, val := range service.Environment.RemoveEmpty().ToMapping() {
|
||||
env[key] = val
|
||||
|
||||
err := s.prepareShellOut(ctx, project.Environment, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd.Env = env.Values()
|
||||
|
||||
// Use docker/cli mechanism to propagate termination signal to child process
|
||||
server, err := socket.NewPluginServer(nil)
|
||||
if err == nil {
|
||||
defer server.Close() //nolint:errcheck
|
||||
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
|
||||
|
||||
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
|
||||
carrier := propagation.MapCarrier{}
|
||||
otel.GetTextMapPropagator().Inject(ctx, &carrier)
|
||||
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (s *composeService) getPluginMetadata(path, command string) ProviderMetadata {
|
||||
func (s *composeService) getPluginMetadata(path, command string, project *types.Project) ProviderMetadata {
|
||||
cmd := exec.Command(path, "compose", "metadata")
|
||||
err := s.prepareShellOut(context.Background(), project.Environment, cmd)
|
||||
if err != nil {
|
||||
logrus.Debugf("failed to prepare plugin metadata command: %v", err)
|
||||
return ProviderMetadata{}
|
||||
}
|
||||
stdout := &bytes.Buffer{}
|
||||
cmd.Stdout = stdout
|
||||
|
||||
@ -255,6 +246,10 @@ type ProviderMetadata struct {
|
||||
Down CommandMetadata `json:"down"`
|
||||
}
|
||||
|
||||
func (p ProviderMetadata) IsEmpty() bool {
|
||||
return p.Description == "" && p.Up.Parameters == nil && p.Down.Parameters == nil
|
||||
}
|
||||
|
||||
type CommandMetadata struct {
|
||||
Parameters []ParameterMetadata `json:"parameters"`
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
@ -26,137 +25,29 @@ import (
|
||||
// logPrinter watch application containers and collect their logs
|
||||
type logPrinter interface {
|
||||
HandleEvent(event api.ContainerEvent)
|
||||
Run(cascade api.Cascade, exitCodeFrom string, stopFn func() error) (int, error)
|
||||
Cancel()
|
||||
Stop()
|
||||
}
|
||||
|
||||
type printer struct {
|
||||
queue chan api.ContainerEvent
|
||||
consumer api.LogConsumer
|
||||
stopCh chan struct{} // stopCh is a signal channel for producers to stop sending events to the queue
|
||||
stop sync.Once
|
||||
}
|
||||
|
||||
// newLogPrinter builds a LogPrinter passing containers logs to LogConsumer
|
||||
func newLogPrinter(consumer api.LogConsumer) logPrinter {
|
||||
printer := printer{
|
||||
consumer: consumer,
|
||||
queue: make(chan api.ContainerEvent),
|
||||
stopCh: make(chan struct{}),
|
||||
stop: sync.Once{},
|
||||
}
|
||||
return &printer
|
||||
}
|
||||
|
||||
func (p *printer) Cancel() {
|
||||
// note: HandleEvent is used to ensure this doesn't deadlock
|
||||
p.HandleEvent(api.ContainerEvent{Type: api.UserCancel})
|
||||
}
|
||||
|
||||
func (p *printer) Stop() {
|
||||
p.stop.Do(func() {
|
||||
close(p.stopCh)
|
||||
for {
|
||||
select {
|
||||
case <-p.queue:
|
||||
// purge the queue to free producers goroutines
|
||||
// p.queue will be garbage collected
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *printer) HandleEvent(event api.ContainerEvent) {
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return
|
||||
default:
|
||||
p.queue <- event
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() error) (int, error) {
|
||||
var (
|
||||
aborting bool
|
||||
exitCode int
|
||||
)
|
||||
defer p.Stop()
|
||||
|
||||
// containers we are tracking. Use true when container is running, false after we receive a stop|die signal
|
||||
containers := map[string]bool{}
|
||||
for {
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return exitCode, nil
|
||||
case event := <-p.queue:
|
||||
container, id := event.Container, event.ID
|
||||
switch event.Type {
|
||||
case api.UserCancel:
|
||||
aborting = true
|
||||
case api.ContainerEventAttach:
|
||||
if attached, ok := containers[id]; ok && attached {
|
||||
continue
|
||||
}
|
||||
containers[id] = true
|
||||
p.consumer.Register(container)
|
||||
case api.ContainerEventExit, api.ContainerEventStopped, api.ContainerEventRecreated:
|
||||
if !aborting && containers[id] {
|
||||
p.consumer.Status(container, fmt.Sprintf("exited with code %d", event.ExitCode))
|
||||
if event.Type == api.ContainerEventRecreated {
|
||||
p.consumer.Status(container, "has been recreated")
|
||||
}
|
||||
}
|
||||
containers[id] = false
|
||||
if !event.Restarting {
|
||||
delete(containers, id)
|
||||
}
|
||||
|
||||
if cascade == api.CascadeStop {
|
||||
if !aborting {
|
||||
aborting = true
|
||||
err := stopFn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if event.Type == api.ContainerEventExit {
|
||||
if cascade == api.CascadeFail && event.ExitCode != 0 {
|
||||
exitCodeFrom = event.Service
|
||||
if !aborting {
|
||||
aborting = true
|
||||
err := stopFn()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if cascade == api.CascadeStop && exitCodeFrom == "" {
|
||||
exitCodeFrom = event.Service
|
||||
}
|
||||
}
|
||||
|
||||
if exitCodeFrom == event.Service && (event.Type == api.ContainerEventExit || event.Type == api.ContainerEventStopped) {
|
||||
// Container was interrupted or exited, let's capture exit code
|
||||
exitCode = event.ExitCode
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
// Last container terminated, done
|
||||
return exitCode, nil
|
||||
}
|
||||
case api.ContainerEventLog, api.HookEventLog:
|
||||
if !aborting {
|
||||
p.consumer.Log(container, event.Line)
|
||||
}
|
||||
case api.ContainerEventErr:
|
||||
if !aborting {
|
||||
p.consumer.Err(container, event.Line)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch event.Type {
|
||||
case api.ContainerEventExited:
|
||||
p.consumer.Status(event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
|
||||
case api.ContainerEventRecreated:
|
||||
p.consumer.Status(event.Container.Labels[api.ContainerReplaceLabel], "has been recreated")
|
||||
case api.ContainerEventLog, api.HookEventLog:
|
||||
p.consumer.Log(event.Source, event.Line)
|
||||
case api.ContainerEventErr:
|
||||
p.consumer.Err(event.Source, event.Line)
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@ -42,13 +43,13 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
|
||||
}
|
||||
summary := make([]api.ContainerSummary, len(containers))
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for i, container := range containers {
|
||||
for i, ctr := range containers {
|
||||
eg.Go(func() error {
|
||||
publishers := make([]api.PortPublisher, len(container.Ports))
|
||||
sort.Slice(container.Ports, func(i, j int) bool {
|
||||
return container.Ports[i].PrivatePort < container.Ports[j].PrivatePort
|
||||
publishers := make([]api.PortPublisher, len(ctr.Ports))
|
||||
sort.Slice(ctr.Ports, func(i, j int) bool {
|
||||
return ctr.Ports[i].PrivatePort < ctr.Ports[j].PrivatePort
|
||||
})
|
||||
for i, p := range container.Ports {
|
||||
for i, p := range ctr.Ports {
|
||||
publishers[i] = api.PortPublisher{
|
||||
URL: p.IP,
|
||||
TargetPort: int(p.PrivatePort),
|
||||
@ -57,22 +58,22 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
|
||||
}
|
||||
}
|
||||
|
||||
inspect, err := s.apiClient().ContainerInspect(ctx, container.ID)
|
||||
inspect, err := s.apiClient().ContainerInspect(ctx, ctr.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
health string
|
||||
health container.HealthStatus
|
||||
exitCode int
|
||||
)
|
||||
if inspect.State != nil {
|
||||
switch inspect.State.Status {
|
||||
case "running":
|
||||
case container.StateRunning:
|
||||
if inspect.State.Health != nil {
|
||||
health = inspect.State.Health.Status
|
||||
}
|
||||
case "exited", "dead":
|
||||
case container.StateExited, container.StateDead:
|
||||
exitCode = inspect.State.ExitCode
|
||||
}
|
||||
}
|
||||
@ -81,7 +82,7 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
|
||||
local int
|
||||
mounts []string
|
||||
)
|
||||
for _, m := range container.Mounts {
|
||||
for _, m := range ctr.Mounts {
|
||||
name := m.Name
|
||||
if name == "" {
|
||||
name = m.Source
|
||||
@ -93,26 +94,26 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
|
||||
}
|
||||
|
||||
var networks []string
|
||||
if container.NetworkSettings != nil {
|
||||
for k := range container.NetworkSettings.Networks {
|
||||
if ctr.NetworkSettings != nil {
|
||||
for k := range ctr.NetworkSettings.Networks {
|
||||
networks = append(networks, k)
|
||||
}
|
||||
}
|
||||
|
||||
summary[i] = api.ContainerSummary{
|
||||
ID: container.ID,
|
||||
Name: getCanonicalContainerName(container),
|
||||
Names: container.Names,
|
||||
Image: container.Image,
|
||||
Project: container.Labels[api.ProjectLabel],
|
||||
Service: container.Labels[api.ServiceLabel],
|
||||
Command: container.Command,
|
||||
State: container.State,
|
||||
Status: container.Status,
|
||||
Created: container.Created,
|
||||
Labels: container.Labels,
|
||||
SizeRw: container.SizeRw,
|
||||
SizeRootFs: container.SizeRootFs,
|
||||
ID: ctr.ID,
|
||||
Name: getCanonicalContainerName(ctr),
|
||||
Names: ctr.Names,
|
||||
Image: ctr.Image,
|
||||
Project: ctr.Labels[api.ProjectLabel],
|
||||
Service: ctr.Labels[api.ServiceLabel],
|
||||
Command: ctr.Command,
|
||||
State: ctr.State,
|
||||
Status: ctr.Status,
|
||||
Created: ctr.Created,
|
||||
Labels: ctr.Labels,
|
||||
SizeRw: ctr.SizeRw,
|
||||
SizeRootFs: ctr.SizeRootFs,
|
||||
Mounts: mounts,
|
||||
LocalVolumes: local,
|
||||
Networks: networks,
|
||||
|
@ -22,11 +22,11 @@ import (
|
||||
"testing"
|
||||
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
compose "github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
|
||||
func TestPs(t *testing.T) {
|
||||
@ -42,10 +42,10 @@ func TestPs(t *testing.T) {
|
||||
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)), hasConfigHashLabel())
|
||||
args.Add("label", "com.docker.compose.oneoff=False")
|
||||
listOpts := containerType.ListOptions{Filters: args, All: false}
|
||||
c1, inspect1 := containerDetails("service1", "123", "running", "healthy", 0)
|
||||
c2, inspect2 := containerDetails("service1", "456", "running", "", 0)
|
||||
c1, inspect1 := containerDetails("service1", "123", containerType.StateRunning, containerType.Healthy, 0)
|
||||
c2, inspect2 := containerDetails("service1", "456", containerType.StateRunning, "", 0)
|
||||
c2.Ports = []containerType.Port{{PublicPort: 80, PrivatePort: 90, IP: "localhost"}}
|
||||
c3, inspect3 := containerDetails("service2", "789", "exited", "", 130)
|
||||
c3, inspect3 := containerDetails("service2", "789", containerType.StateExited, "", 130)
|
||||
api.EXPECT().ContainerList(ctx, listOpts).Return([]containerType.Summary{c1, c2, c3}, nil)
|
||||
api.EXPECT().ContainerInspect(anyCancellableContext(), "123").Return(inspect1, nil)
|
||||
api.EXPECT().ContainerInspect(anyCancellableContext(), "456").Return(inspect2, nil)
|
||||
@ -56,7 +56,9 @@ func TestPs(t *testing.T) {
|
||||
expected := []compose.ContainerSummary{
|
||||
{
|
||||
ID: "123", Name: "123", Names: []string{"/123"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
|
||||
State: "running", Health: "healthy", Publishers: []compose.PortPublisher{},
|
||||
State: containerType.StateRunning,
|
||||
Health: containerType.Healthy,
|
||||
Publishers: []compose.PortPublisher{},
|
||||
Labels: map[string]string{
|
||||
compose.ProjectLabel: strings.ToLower(testProject),
|
||||
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
|
||||
@ -66,7 +68,8 @@ func TestPs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
ID: "456", Name: "456", Names: []string{"/456"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
|
||||
State: "running", Health: "",
|
||||
State: containerType.StateRunning,
|
||||
Health: "",
|
||||
Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90, PublishedPort: 80}},
|
||||
Labels: map[string]string{
|
||||
compose.ProjectLabel: strings.ToLower(testProject),
|
||||
@ -77,7 +80,10 @@ func TestPs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
ID: "789", Name: "789", Names: []string{"/789"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service2",
|
||||
State: "exited", Health: "", ExitCode: 130, Publishers: []compose.PortPublisher{},
|
||||
State: containerType.StateExited,
|
||||
Health: "",
|
||||
ExitCode: 130,
|
||||
Publishers: []compose.PortPublisher{},
|
||||
Labels: map[string]string{
|
||||
compose.ProjectLabel: strings.ToLower(testProject),
|
||||
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
|
||||
@ -90,8 +96,8 @@ func TestPs(t *testing.T) {
|
||||
assert.DeepEqual(t, containers, expected)
|
||||
}
|
||||
|
||||
func containerDetails(service string, id string, status string, health string, exitCode int) (containerType.Summary, containerType.InspectResponse) {
|
||||
container := containerType.Summary{
|
||||
func containerDetails(service string, id string, status containerType.ContainerState, health containerType.HealthStatus, exitCode int) (containerType.Summary, containerType.InspectResponse) {
|
||||
ctr := containerType.Summary{
|
||||
ID: id,
|
||||
Names: []string{"/" + id},
|
||||
Image: "foo",
|
||||
@ -107,5 +113,5 @@ func containerDetails(service string, id string, status string, health string, e
|
||||
},
|
||||
},
|
||||
}
|
||||
return container, inspect
|
||||
return ctr, inspect
|
||||
}
|
||||
|
@ -236,12 +236,24 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp
|
||||
if ok, err := s.checkOnlyBuildSection(project); !ok || err != nil {
|
||||
return false, err
|
||||
}
|
||||
if ok, err := s.checkForBindMount(project); !ok || err != nil {
|
||||
return false, err
|
||||
}
|
||||
if options.AssumeYes {
|
||||
return true, nil
|
||||
}
|
||||
bindMounts := s.checkForBindMount(project)
|
||||
if len(bindMounts) > 0 {
|
||||
fmt.Println("you are about to publish bind mounts declaration within your OCI artifact.\n" +
|
||||
"only the bind mount declarations will be added to the OCI artifact (not content)\n" +
|
||||
"please double check that you are not mounting potential user's sensitive directories or data")
|
||||
for key, val := range bindMounts {
|
||||
_, _ = fmt.Fprintln(s.dockerCli.Out(), key)
|
||||
for _, v := range val {
|
||||
_, _ = fmt.Fprintf(s.dockerCli.Out(), "%s\n", v.String())
|
||||
}
|
||||
}
|
||||
if ok, err := acceptPublishBindMountDeclarations(s.dockerCli); err != nil || !ok {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
detectedSecrets, err := s.checkForSensitiveData(project)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -325,6 +337,12 @@ func acceptPublishSensitiveData(cli command.Cli) (bool, error) {
|
||||
return confirm, err
|
||||
}
|
||||
|
||||
func acceptPublishBindMountDeclarations(cli command.Cli) (bool, error) {
|
||||
msg := "Are you ok to publish these bind mount declarations? [y/N]: "
|
||||
confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false)
|
||||
return confirm, err
|
||||
}
|
||||
|
||||
func envFileLayers(project *types.Project) []ocipush.Pushable {
|
||||
var layers []ocipush.Pushable
|
||||
for _, service := range project.Services {
|
||||
@ -361,15 +379,20 @@ func (s *composeService) checkOnlyBuildSection(project *types.Project) (bool, er
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *composeService) checkForBindMount(project *types.Project) (bool, error) {
|
||||
for name, config := range project.Services {
|
||||
func (s *composeService) checkForBindMount(project *types.Project) map[string][]types.ServiceVolumeConfig {
|
||||
allFindings := map[string][]types.ServiceVolumeConfig{}
|
||||
for serviceName, config := range project.Services {
|
||||
bindMounts := []types.ServiceVolumeConfig{}
|
||||
for _, volume := range config.Volumes {
|
||||
if volume.Type == types.VolumeTypeBind {
|
||||
return false, fmt.Errorf("cannot publish compose file: service %q relies on bind-mount. You should use volumes", name)
|
||||
bindMounts = append(bindMounts, volume)
|
||||
}
|
||||
}
|
||||
if len(bindMounts) > 0 {
|
||||
allFindings[serviceName] = bindMounts
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
return allFindings
|
||||
}
|
||||
|
||||
func (s *composeService) checkForSensitiveData(project *types.Project) ([]secrets.DetectedSecret, error) {
|
||||
|
@ -34,11 +34,10 @@ import (
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/internal/registry"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
@ -152,7 +151,7 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
|
||||
if opts.IgnoreFailures {
|
||||
return nil
|
||||
}
|
||||
return multierror.Append(nil, pullErrors...).ErrorOrNil()
|
||||
return errors.Join(pullErrors...)
|
||||
}
|
||||
|
||||
func imageAlreadyPresent(serviceImage string, localImages map[string]api.ImageSummary) bool {
|
||||
@ -281,13 +280,7 @@ func ImageDigestResolver(ctx context.Context, file *configfile.ConfigFile, apiCl
|
||||
}
|
||||
|
||||
func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key := registry.GetAuthConfigKey(repoInfo.Index)
|
||||
authConfig, err := configFile.GetAuthConfig(key)
|
||||
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(ref))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -29,11 +29,10 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/driver"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/internal/registry"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
@ -51,14 +50,6 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
eg.SetLimit(s.maxConcurrency)
|
||||
|
||||
info, err := s.apiClient().Info(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IndexServerAddress == "" {
|
||||
info.IndexServerAddress = registry.IndexServer
|
||||
}
|
||||
|
||||
w := progress.ContextWriter(ctx)
|
||||
for _, service := range project.Services {
|
||||
if service.Build == nil || service.Image == "" {
|
||||
@ -79,7 +70,7 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
|
||||
|
||||
for _, tag := range tags {
|
||||
eg.Go(func() error {
|
||||
err := s.pushServiceImage(ctx, tag, info, s.configFile(), w, options.Quiet)
|
||||
err := s.pushServiceImage(ctx, tag, s.configFile(), w, options.Quiet)
|
||||
if err != nil {
|
||||
if !options.IgnoreFailures {
|
||||
return err
|
||||
@ -93,22 +84,13 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) pushServiceImage(ctx context.Context, tag string, info system.Info, configFile driver.Auth, w progress.Writer, quietPush bool) error {
|
||||
func (s *composeService) pushServiceImage(ctx context.Context, tag string, configFile driver.Auth, w progress.Writer, quietPush bool) error {
|
||||
ref, err := reference.ParseNormalizedNamed(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := repoInfo.Index.Name
|
||||
if repoInfo.Index.Official {
|
||||
key = info.IndexServerAddress
|
||||
}
|
||||
authConfig, err := configFile.GetAuthConfig(key)
|
||||
authConfig, err := configFile.GetAuthConfig(registry.GetAuthConfigKey(ref))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ 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"
|
||||
)
|
||||
|
||||
@ -58,6 +59,19 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
||||
}
|
||||
|
||||
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
|
||||
// Temporary implementation of use_api_socket until we get actual support inside docker engine
|
||||
project, err := s.useAPISocket(project)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = progress.Run(ctx, func(ctx context.Context) error {
|
||||
return s.startDependencies(ctx, project, opts)
|
||||
}, s.stdinfo())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
service, err := project.GetService(opts.Service)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -109,11 +123,23 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = s.ensureModels(ctx, project, opts.QuietPull)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
created, err := s.createContainer(ctx, project, service, service.ContainerName, -1, createOpts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return created.ID, nil
|
||||
|
||||
err = s.injectSecrets(ctx, project, service, created.ID)
|
||||
if err != nil {
|
||||
return created.ID, err
|
||||
}
|
||||
|
||||
err = s.injectConfigs(ctx, project, service, created.ID)
|
||||
return created.ID, err
|
||||
}
|
||||
|
||||
func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts api.RunOptions) {
|
||||
@ -160,3 +186,24 @@ 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
|
||||
}
|
||||
|
61
pkg/compose/shellout.go
Normal file
61
pkg/compose/shellout.go
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/context/docker"
|
||||
"github.com/docker/compose/v2/internal"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
)
|
||||
|
||||
// prepareShellOut prepare a shell-out command to be ran by Compose
|
||||
func (s *composeService) prepareShellOut(gctx context.Context, env types.Mapping, cmd *exec.Cmd) error {
|
||||
env = env.Clone()
|
||||
// remove DOCKER_CLI_PLUGIN... variable so a docker-cli plugin will detect it run standalone
|
||||
delete(env, metadata.ReexecEnvvar)
|
||||
|
||||
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
|
||||
carrier := propagation.MapCarrier{}
|
||||
otel.GetTextMapPropagator().Inject(gctx, &carrier)
|
||||
env.Merge(types.Mapping(carrier))
|
||||
|
||||
env["DOCKER_CONTEXT"] = s.dockerCli.CurrentContext()
|
||||
env["USER_AGENT"] = "compose/" + internal.Version
|
||||
|
||||
md, err := s.dockerCli.ContextStore().GetMetadata(s.dockerCli.CurrentContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpoint, err := docker.EndpointFromContext(md)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualHost := s.dockerCli.DockerEndpoint().Host
|
||||
if endpoint.Host != actualHost {
|
||||
// We are running with `--host` or `DOCKER_HOST` which overrides selected context
|
||||
env["DOCKER_HOST"] = actualHost
|
||||
}
|
||||
|
||||
cmd.Env = env.Values()
|
||||
return nil
|
||||
}
|
@ -20,19 +20,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
|
||||
@ -56,60 +51,6 @@ func (s *composeService) start(ctx context.Context, projectName string, options
|
||||
}
|
||||
}
|
||||
|
||||
// use an independent context tied to the errgroup for background attach operations
|
||||
// the primary context is still used for other operations
|
||||
// this means that once any attach operation fails, all other attaches are cancelled,
|
||||
// but an attach failing won't interfere with the rest of the start
|
||||
eg, attachCtx := errgroup.WithContext(ctx)
|
||||
if listener != nil {
|
||||
_, err := s.attach(attachCtx, project, listener, options.AttachTo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
// it's possible to have a required service whose log output is not desired
|
||||
// (i.e. it's not in the attach set), so watch everything and then filter
|
||||
// calls to attach; this ensures that `watchContainers` blocks until all
|
||||
// required containers have exited, even if their output is not being shown
|
||||
attachTo := utils.NewSet[string](options.AttachTo...)
|
||||
required := utils.NewSet[string](options.Services...)
|
||||
toWatch := attachTo.Union(required).Elements()
|
||||
|
||||
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true, toWatch...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// N.B. this uses the parent context (instead of attachCtx) so that the watch itself can
|
||||
// continue even if one of the log streams fails
|
||||
return s.watchContainers(ctx, project.Name, toWatch, required.Elements(), listener, containers,
|
||||
func(ctr containerType.Summary, _ time.Time) error {
|
||||
svc := ctr.Labels[api.ServiceLabel]
|
||||
if attachTo.Has(svc) {
|
||||
return s.attachContainer(attachCtx, ctr, listener)
|
||||
}
|
||||
|
||||
// HACK: simulate an "attach" event
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: getContainerNameWithoutProject(ctr),
|
||||
ID: ctr.ID,
|
||||
Service: svc,
|
||||
})
|
||||
return nil
|
||||
}, func(ctr containerType.Summary, _ time.Time) error {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: "", // actual name will be set by start event
|
||||
ID: ctr.ID,
|
||||
Service: ctr.Labels[api.ServiceLabel],
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var containers Containers
|
||||
containers, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
@ -157,7 +98,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
|
||||
}
|
||||
}
|
||||
|
||||
return eg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDependencyCondition checks if service is depended on by other services
|
||||
@ -173,182 +114,3 @@ func getDependencyCondition(service types.ServiceConfig, project *types.Project)
|
||||
}
|
||||
return ServiceConditionRunningOrHealthy
|
||||
}
|
||||
|
||||
type containerWatchFn func(ctr containerType.Summary, t time.Time) error
|
||||
|
||||
// watchContainers uses engine events to capture container start/die and notify ContainerEventListener
|
||||
func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
projectName string, services, required []string,
|
||||
listener api.ContainerEventListener, containers Containers, onStart, onRecreate containerWatchFn,
|
||||
) error {
|
||||
if len(containers) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(required) == 0 {
|
||||
required = services
|
||||
}
|
||||
|
||||
unexpected := utils.NewSet[string](required...).Diff(utils.NewSet[string](services...))
|
||||
if len(unexpected) != 0 {
|
||||
return fmt.Errorf(`required service(s) "%s" not present in watched service(s) "%s"`,
|
||||
strings.Join(unexpected.Elements(), ", "),
|
||||
strings.Join(services, ", "))
|
||||
}
|
||||
|
||||
// predicate to tell if a container we receive event for should be considered or ignored
|
||||
ofInterest := func(c containerType.Summary) bool {
|
||||
if len(services) > 0 {
|
||||
// we only watch some services
|
||||
return slices.Contains(services, c.Labels[api.ServiceLabel])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// predicate to tell if a container we receive event for should be watched until termination
|
||||
isRequired := func(c containerType.Summary) bool {
|
||||
if len(services) > 0 && len(required) > 0 {
|
||||
// we only watch some services
|
||||
return slices.Contains(required, c.Labels[api.ServiceLabel])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
expected = utils.NewSet[string]()
|
||||
watched = map[string]int{}
|
||||
replaced []string
|
||||
)
|
||||
for _, c := range containers {
|
||||
if isRequired(c) {
|
||||
expected.Add(c.ID)
|
||||
}
|
||||
watched[c.ID] = 0
|
||||
}
|
||||
|
||||
ctx, stop := context.WithCancel(ctx)
|
||||
err := s.Events(ctx, projectName, api.EventsOptions{
|
||||
Services: services,
|
||||
Consumer: func(event api.Event) error {
|
||||
defer func() {
|
||||
// after consuming each event, check to see if we're done
|
||||
if len(expected) == 0 {
|
||||
stop()
|
||||
}
|
||||
}()
|
||||
inspected, err := s.apiClient().ContainerInspect(ctx, event.Container)
|
||||
if err != nil {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
// it's possible to get "destroy" or "kill" events but not
|
||||
// be able to inspect in time before they're gone from the
|
||||
// API, so just remove the watch without erroring
|
||||
delete(watched, event.Container)
|
||||
expected.Remove(event.Container)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
container := containerType.Summary{
|
||||
ID: inspected.ID,
|
||||
Names: []string{inspected.Name},
|
||||
Labels: inspected.Config.Labels,
|
||||
}
|
||||
name := getContainerNameWithoutProject(container)
|
||||
service := container.Labels[api.ServiceLabel]
|
||||
switch event.Status {
|
||||
case "stop":
|
||||
if inspected.State.Running {
|
||||
// on sync+restart action the container stops -> dies -> start -> restart
|
||||
// we do not want to stop the current container, we want to restart it
|
||||
return nil
|
||||
}
|
||||
if _, ok := watched[container.ID]; ok {
|
||||
eType := api.ContainerEventStopped
|
||||
if slices.Contains(replaced, container.ID) {
|
||||
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
|
||||
eType = api.ContainerEventRecreated
|
||||
}
|
||||
listener(api.ContainerEvent{
|
||||
Type: eType,
|
||||
Container: name,
|
||||
ID: container.ID,
|
||||
Service: service,
|
||||
ExitCode: inspected.State.ExitCode,
|
||||
})
|
||||
}
|
||||
|
||||
delete(watched, container.ID)
|
||||
expected.Remove(container.ID)
|
||||
case "die":
|
||||
restarted := watched[container.ID]
|
||||
watched[container.ID] = restarted + 1
|
||||
// Container terminated.
|
||||
willRestart := inspected.State.Restarting
|
||||
if inspected.State.Running {
|
||||
// on sync+restart action inspected.State.Restarting is false,
|
||||
// however the container is already running before it restarts
|
||||
willRestart = true
|
||||
}
|
||||
|
||||
eType := api.ContainerEventExit
|
||||
if slices.Contains(replaced, container.ID) {
|
||||
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
|
||||
eType = api.ContainerEventRecreated
|
||||
}
|
||||
|
||||
listener(api.ContainerEvent{
|
||||
Type: eType,
|
||||
Container: name,
|
||||
ID: container.ID,
|
||||
Service: service,
|
||||
ExitCode: inspected.State.ExitCode,
|
||||
Restarting: willRestart,
|
||||
})
|
||||
|
||||
if !willRestart {
|
||||
// we're done with this one
|
||||
delete(watched, container.ID)
|
||||
expected.Remove(container.ID)
|
||||
}
|
||||
case "start":
|
||||
count, ok := watched[container.ID]
|
||||
mustAttach := ok && count > 0 // Container restarted, need to re-attach
|
||||
if !ok {
|
||||
// A new container has just been added to service by scale
|
||||
watched[container.ID] = 0
|
||||
expected.Add(container.ID)
|
||||
mustAttach = true
|
||||
}
|
||||
if mustAttach {
|
||||
// Container restarted, need to re-attach
|
||||
err := onStart(container, event.Timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "create":
|
||||
if id, ok := container.Labels[api.ContainerReplaceLabel]; ok {
|
||||
replaced = append(replaced, id)
|
||||
err = onRecreate(container, event.Timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if expected.Has(id) {
|
||||
expected.Add(inspected.ID)
|
||||
expected.Add(container.ID)
|
||||
}
|
||||
watched[container.ID] = 1
|
||||
} else if ofInterest(container) {
|
||||
watched[container.ID] = 1
|
||||
if isRequired(container) {
|
||||
expected.Add(container.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -27,11 +27,11 @@ import (
|
||||
|
||||
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
return s.stop(ctx, strings.ToLower(projectName), options)
|
||||
return s.stop(ctx, strings.ToLower(projectName), options, nil)
|
||||
}, s.stdinfo(), "Stopping")
|
||||
}
|
||||
|
||||
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
||||
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions, event api.ContainerEventListener) error {
|
||||
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -55,6 +55,6 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a
|
||||
return nil
|
||||
}
|
||||
serv := project.Services[service]
|
||||
return s.stopContainers(ctx, w, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout)
|
||||
return s.stopContainers(ctx, w, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout, event)
|
||||
})
|
||||
}
|
||||
|
@ -18,22 +18,25 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/eiannone/keyboard"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo
|
||||
@ -59,26 +62,20 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
return err
|
||||
}
|
||||
|
||||
var eg multierror.Group
|
||||
|
||||
// if we get a second signal during shutdown, we kill the services
|
||||
// immediately, so the channel needs to have sufficient capacity or
|
||||
// we might miss a signal while setting up the second channel read
|
||||
// (this is also why signal.Notify is used vs signal.NotifyContext)
|
||||
signalChan := make(chan os.Signal, 2)
|
||||
defer close(signalChan)
|
||||
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer signal.Stop(signalChan)
|
||||
var isTerminated atomic.Bool
|
||||
printer := newLogPrinter(options.Start.Attach)
|
||||
|
||||
watcher, err := NewWatcher(project, options, s.watch)
|
||||
if err != nil && options.Start.Watch {
|
||||
return err
|
||||
}
|
||||
|
||||
var navigationMenu *formatter.LogKeyboard
|
||||
var kEvents <-chan keyboard.KeyEvent
|
||||
var (
|
||||
logConsumer = options.Start.Attach
|
||||
navigationMenu *formatter.LogKeyboard
|
||||
kEvents <-chan keyboard.KeyEvent
|
||||
)
|
||||
if options.Start.NavigationMenu {
|
||||
kEvents, err = keyboard.GetKeys(100)
|
||||
if err != nil {
|
||||
@ -87,31 +84,62 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
} else {
|
||||
defer keyboard.Close() //nolint:errcheck
|
||||
isDockerDesktopActive := s.isDesktopIntegrationActive()
|
||||
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, watcher != nil)
|
||||
navigationMenu = formatter.NewKeyboardManager(isDockerDesktopActive, signalChan, options.Start.Watch, watcher)
|
||||
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive)
|
||||
navigationMenu = formatter.NewKeyboardManager(isDockerDesktopActive, signalChan)
|
||||
logConsumer = navigationMenu.Decorate(logConsumer)
|
||||
}
|
||||
}
|
||||
|
||||
watcher, err := NewWatcher(project, options, s.watch, logConsumer)
|
||||
if err != nil && options.Start.Watch {
|
||||
return err
|
||||
}
|
||||
|
||||
if navigationMenu != nil && watcher != nil {
|
||||
navigationMenu.EnableWatch(options.Start.Watch, watcher)
|
||||
}
|
||||
|
||||
printer := newLogPrinter(logConsumer)
|
||||
|
||||
// global context to handle canceling goroutines
|
||||
globalCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var (
|
||||
eg errgroup.Group
|
||||
mu sync.Mutex
|
||||
errs []error
|
||||
)
|
||||
|
||||
appendErr := func(err error) {
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
errs = append(errs, err)
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
doneCh := make(chan bool)
|
||||
eg.Go(func() error {
|
||||
first := true
|
||||
gracefulTeardown := func() {
|
||||
printer.Cancel()
|
||||
_, _ = fmt.Fprintln(s.stdinfo(), "Gracefully stopping... (press Ctrl+C again to force)")
|
||||
eg.Go(func() error {
|
||||
err := s.Stop(context.WithoutCancel(ctx), project.Name, api.StopOptions{
|
||||
Services: options.Create.Services,
|
||||
Project: project,
|
||||
})
|
||||
isTerminated.Store(true)
|
||||
return err
|
||||
})
|
||||
first = false
|
||||
fmt.Println("Gracefully Stopping... press Ctrl+C again to force")
|
||||
eg.Go(func() error {
|
||||
err := progress.RunWithLog(context.WithoutCancel(globalCtx), func(c context.Context) error {
|
||||
return s.stop(c, project.Name, api.StopOptions{
|
||||
Services: options.Create.Services,
|
||||
Project: project,
|
||||
}, printer.HandleEvent)
|
||||
}, s.stdinfo(), logConsumer)
|
||||
appendErr(err)
|
||||
return nil
|
||||
})
|
||||
isTerminated.Store(true)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-doneCh:
|
||||
case <-globalCtx.Done():
|
||||
if watcher != nil {
|
||||
return watcher.Stop()
|
||||
}
|
||||
@ -122,64 +150,145 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
}
|
||||
case <-signalChan:
|
||||
if first {
|
||||
keyboard.Close() //nolint:errcheck
|
||||
_ = keyboard.Close()
|
||||
gracefulTeardown()
|
||||
break
|
||||
}
|
||||
eg.Go(func() error {
|
||||
err := s.kill(context.WithoutCancel(ctx), project.Name, api.KillOptions{
|
||||
err := s.kill(context.WithoutCancel(globalCtx), project.Name, api.KillOptions{
|
||||
Services: options.Create.Services,
|
||||
Project: project,
|
||||
All: true,
|
||||
})
|
||||
// Ignore errors indicating that some of the containers were already stopped or removed.
|
||||
if cerrdefs.IsNotFound(err) || cerrdefs.IsConflict(err) {
|
||||
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
appendErr(err)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
case event := <-kEvents:
|
||||
navigationMenu.HandleKeyEvents(ctx, event, project, options)
|
||||
navigationMenu.HandleKeyEvents(globalCtx, event, project, options)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var exitCode int
|
||||
eg.Go(func() error {
|
||||
code, err := printer.Run(options.Start.OnExit, options.Start.ExitCodeFrom, func() error {
|
||||
_, _ = fmt.Fprintln(s.stdinfo(), "Aborting on container exit...")
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return s.Stop(ctx, project.Name, api.StopOptions{
|
||||
Services: options.Create.Services,
|
||||
Project: project,
|
||||
})
|
||||
}, s.stdinfo())
|
||||
})
|
||||
exitCode = code
|
||||
return err
|
||||
})
|
||||
|
||||
if options.Start.Watch && watcher != nil {
|
||||
err = watcher.Start(ctx)
|
||||
if err != nil {
|
||||
if err := watcher.Start(globalCtx); err != nil {
|
||||
// cancel the global context to terminate background goroutines
|
||||
cancel()
|
||||
_ = eg.Wait()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
monitor := newMonitor(s.apiClient(), project.Name)
|
||||
if len(options.Start.Services) > 0 {
|
||||
monitor.withServices(options.Start.Services)
|
||||
} else {
|
||||
// Start.AttachTo have been already curated with only the services to monitor
|
||||
monitor.withServices(options.Start.AttachTo)
|
||||
}
|
||||
monitor.withListener(printer.HandleEvent)
|
||||
|
||||
var exitCode int
|
||||
if options.Start.OnExit != api.CascadeIgnore {
|
||||
once := true
|
||||
// detect first container to exit to trigger application shutdown
|
||||
monitor.withListener(func(event api.ContainerEvent) {
|
||||
if once && event.Type == api.ContainerEventExited {
|
||||
if options.Start.OnExit == api.CascadeFail && event.ExitCode == 0 {
|
||||
return
|
||||
}
|
||||
once = false
|
||||
exitCode = event.ExitCode
|
||||
_, _ = fmt.Fprintln(s.stdinfo(), progress.ErrorColor("Aborting on container exit..."))
|
||||
eg.Go(func() error {
|
||||
err := progress.RunWithLog(context.WithoutCancel(globalCtx), func(c context.Context) error {
|
||||
return s.stop(c, project.Name, api.StopOptions{
|
||||
Services: options.Create.Services,
|
||||
Project: project,
|
||||
}, printer.HandleEvent)
|
||||
}, s.stdinfo(), logConsumer)
|
||||
appendErr(err)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if options.Start.ExitCodeFrom != "" {
|
||||
once := true
|
||||
// capture exit code from first container to exit with selected service
|
||||
monitor.withListener(func(event api.ContainerEvent) {
|
||||
if once && event.Type == api.ContainerEventExited && event.Service == options.Start.ExitCodeFrom {
|
||||
exitCode = event.ExitCode
|
||||
once = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
containers, err := s.attach(globalCtx, project, printer.HandleEvent, options.Start.AttachTo)
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = eg.Wait()
|
||||
return err
|
||||
}
|
||||
attached := make([]string, len(containers))
|
||||
for i, ctr := range containers {
|
||||
attached[i] = ctr.ID
|
||||
}
|
||||
|
||||
monitor.withListener(func(event api.ContainerEvent) {
|
||||
if event.Type != api.ContainerEventStarted {
|
||||
return
|
||||
}
|
||||
if slices.Contains(attached, event.ID) {
|
||||
return
|
||||
}
|
||||
eg.Go(func() error {
|
||||
ctr, err := s.apiClient().ContainerInspect(globalCtx, event.ID)
|
||||
if err != nil {
|
||||
appendErr(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.doLogContainer(globalCtx, options.Start.Attach, event.Source, ctr, api.LogOptions{
|
||||
Follow: true,
|
||||
Since: ctr.State.StartedAt,
|
||||
})
|
||||
if errdefs.IsNotImplemented(err) {
|
||||
// container may be configured with logging_driver: none
|
||||
// as container already started, we might miss the very first logs. But still better than none
|
||||
err := s.doAttachContainer(globalCtx, event.Service, event.ID, event.Source, printer.HandleEvent)
|
||||
appendErr(err)
|
||||
return nil
|
||||
}
|
||||
appendErr(err)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
err := monitor.Start(globalCtx)
|
||||
// cancel the global context to terminate signal-handler goroutines
|
||||
cancel()
|
||||
appendErr(err)
|
||||
return nil
|
||||
})
|
||||
|
||||
// We use the parent context without cancellation as we manage sigterm to stop the stack
|
||||
err = s.start(context.WithoutCancel(ctx), project.Name, options.Start, printer.HandleEvent)
|
||||
if err != nil && !isTerminated.Load() { // Ignore error if the process is terminated
|
||||
cancel()
|
||||
_ = eg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
// Signal for the signal-handler goroutines to stop
|
||||
close(doneCh)
|
||||
|
||||
printer.Stop()
|
||||
|
||||
err = eg.Wait().ErrorOrNil()
|
||||
_ = eg.Wait()
|
||||
err = errors.Join(errs...)
|
||||
if exitCode != 0 {
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
|
85
pkg/compose/volumes.go
Normal file
85
pkg/compose/volumes.go
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
)
|
||||
|
||||
func (s *composeService) Volumes(ctx context.Context, project *types.Project, options api.VolumesOptions) ([]api.VolumesSummary, error) {
|
||||
projectName := project.Name
|
||||
|
||||
allContainers, err := s.apiClient().ContainerList(ctx, container.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(projectName)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers []container.Summary
|
||||
|
||||
if len(options.Services) > 0 {
|
||||
// filter service containers
|
||||
for _, c := range allContainers {
|
||||
if slices.Contains(options.Services, c.Labels[api.ServiceLabel]) {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
containers = allContainers
|
||||
}
|
||||
|
||||
volumesResponse, err := s.apiClient().VolumeList(ctx, volume.ListOptions{
|
||||
Filters: filters.NewArgs(projectFilter(projectName)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectVolumes := volumesResponse.Volumes
|
||||
|
||||
if len(options.Services) == 0 {
|
||||
return projectVolumes, nil
|
||||
}
|
||||
|
||||
var volumes []api.VolumesSummary
|
||||
|
||||
// create a name lookup of volumes used by containers
|
||||
serviceVolumes := make(map[string]bool)
|
||||
|
||||
for _, container := range containers {
|
||||
for _, mount := range container.Mounts {
|
||||
serviceVolumes[mount.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// append if volumes in this project are in serviceVolumes
|
||||
for _, v := range projectVolumes {
|
||||
if serviceVolumes[v.Name] {
|
||||
volumes = append(volumes, v)
|
||||
}
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
89
pkg/compose/volumes_test.go
Normal file
89
pkg/compose/volumes_test.go
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"go.uber.org/mock/gomock"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestVolumes(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockApi, mockCli := prepareMocks(mockCtrl)
|
||||
tested := composeService{
|
||||
dockerCli: mockCli,
|
||||
}
|
||||
|
||||
// Create test volumes
|
||||
vol1 := &volume.Volume{Name: testProject + "_vol1"}
|
||||
vol2 := &volume.Volume{Name: testProject + "_vol2"}
|
||||
vol3 := &volume.Volume{Name: testProject + "_vol3"}
|
||||
|
||||
// Create test containers with volume mounts
|
||||
c1 := container.Summary{
|
||||
Labels: map[string]string{api.ServiceLabel: "service1"},
|
||||
Mounts: []container.MountPoint{
|
||||
{Name: testProject + "_vol1"},
|
||||
{Name: testProject + "_vol2"},
|
||||
},
|
||||
}
|
||||
c2 := container.Summary{
|
||||
Labels: map[string]string{api.ServiceLabel: "service2"},
|
||||
Mounts: []container.MountPoint{
|
||||
{Name: testProject + "_vol3"},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
project := &types.Project{Name: testProject}
|
||||
args := filters.NewArgs(projectFilter(testProject))
|
||||
listOpts := container.ListOptions{Filters: args}
|
||||
volumeListArgs := filters.NewArgs(projectFilter(testProject))
|
||||
volumeListOpts := volume.ListOptions{Filters: volumeListArgs}
|
||||
volumeReturn := volume.ListResponse{
|
||||
Volumes: []*volume.Volume{vol1, vol2, vol3},
|
||||
}
|
||||
containerReturn := []container.Summary{c1, c2}
|
||||
|
||||
// Mock API calls
|
||||
mockApi.EXPECT().ContainerList(ctx, listOpts).Times(2).Return(containerReturn, nil)
|
||||
mockApi.EXPECT().VolumeList(ctx, volumeListOpts).Times(2).Return(volumeReturn, nil)
|
||||
|
||||
// Test without service filter - should return all project volumes
|
||||
volumeOptions := api.VolumesOptions{}
|
||||
volumes, err := tested.Volumes(ctx, project, volumeOptions)
|
||||
expected := []api.VolumesSummary{vol1, vol2, vol3}
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, volumes, expected)
|
||||
|
||||
// Test with service filter - should only return volumes used by service1
|
||||
volumeOptions = api.VolumesOptions{Services: []string{"service1"}}
|
||||
volumes, err = tested.Volumes(ctx, project, volumeOptions)
|
||||
expected = []api.VolumesSummary{vol1, vol2}
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, volumes, expected)
|
||||
}
|
@ -29,14 +29,17 @@ import (
|
||||
gsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/compose-spec/compose-go/v2/utils"
|
||||
ccli "github.com/docker/cli/cli/command/container"
|
||||
pathutil "github.com/docker/compose/v2/internal/paths"
|
||||
"github.com/docker/compose/v2/internal/sync"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
cutils "github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/compose/v2/pkg/watch"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/compose-spec/compose-go/v2/utils"
|
||||
ccli "github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
@ -55,17 +58,16 @@ type Watcher struct {
|
||||
errCh chan error
|
||||
}
|
||||
|
||||
func NewWatcher(project *types.Project, options api.UpOptions, w WatchFunc) (*Watcher, error) {
|
||||
func NewWatcher(project *types.Project, options api.UpOptions, w WatchFunc, consumer api.LogConsumer) (*Watcher, error) {
|
||||
for i := range project.Services {
|
||||
service := project.Services[i]
|
||||
|
||||
if service.Develop != nil && service.Develop.Watch != nil {
|
||||
build := options.Create.Build
|
||||
build.Quiet = true
|
||||
return &Watcher{
|
||||
project: project,
|
||||
options: api.WatchOptions{
|
||||
LogTo: options.Start.Attach,
|
||||
LogTo: consumer,
|
||||
Build: build,
|
||||
},
|
||||
watchFn: w,
|
||||
@ -192,7 +194,6 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti
|
||||
return nil, err
|
||||
}
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
options.LogTo.Register(api.WatchLogger)
|
||||
|
||||
var (
|
||||
rules []watchRule
|
||||
@ -234,7 +235,7 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti
|
||||
var initialSync bool
|
||||
success, err := trigger.Extensions.Get("x-initialSync", &initialSync)
|
||||
if err == nil && success && initialSync && isSync(trigger) {
|
||||
// Need to check initial files are in container that are meant to be synched from watch action
|
||||
// Need to check initial files are in container that are meant to be synced from watch action
|
||||
err := s.initialSync(ctx, project, service, trigger, syncer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -395,7 +396,7 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
|
||||
return nil, fmt.Errorf("service %s doesn't have a build section, can't apply %s on watch", types.WatchActionRebuild, service.Name)
|
||||
}
|
||||
if trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 {
|
||||
return nil, fmt.Errorf("can't watch with action %q on service %s wihtout a command", types.WatchActionSyncExec, service.Name)
|
||||
return nil, fmt.Errorf("can't watch with action %q on service %s without a command", types.WatchActionSyncExec, service.Name)
|
||||
}
|
||||
|
||||
config.Watch[i] = trigger
|
||||
@ -599,6 +600,10 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se
|
||||
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service(s) %q after changes were detected...", services))
|
||||
// restrict the build to ONLY this service, not any of its dependencies
|
||||
options.Build.Services = services
|
||||
options.Build.Progress = progress.ModePlain
|
||||
options.Build.Out = cutils.GetWriter(func(line string) {
|
||||
options.LogTo.Log(api.WatchLogger, line)
|
||||
})
|
||||
|
||||
var (
|
||||
imageNameToIdMap map[string]string
|
||||
@ -813,7 +818,7 @@ func (s *composeService) imageCreatedTime(ctx context.Context, project *types.Pr
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
// Need to get oldest one?
|
||||
// Need to get the oldest one?
|
||||
timeCreated, err := time.Parse(time.RFC3339Nano, img.Created)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
|
@ -71,9 +71,6 @@ func (s stdLogger) Status(containerName, msg string) {
|
||||
fmt.Printf("%s: %s\n", containerName, msg)
|
||||
}
|
||||
|
||||
func (s stdLogger) Register(containerName string) {
|
||||
}
|
||||
|
||||
func TestWatch_Sync(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
cli := mocks.NewMockCli(mockCtrl)
|
||||
|
@ -580,3 +580,31 @@ func TestBuildSubDependencies(t *testing.T) {
|
||||
out = res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "main Built"))
|
||||
}
|
||||
|
||||
func TestBuildLongOutputLine(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "down", "--rmi=local")
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "build", "long-line")
|
||||
out := res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "long-line Built"))
|
||||
|
||||
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "up", "--build", "long-line")
|
||||
out = res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "long-line Built"))
|
||||
}
|
||||
|
||||
func TestBuildDependentImageWithProfile(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "down", "--rmi=local")
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "build", "secret-build-test")
|
||||
out := res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "secret-build-test Built"))
|
||||
}
|
||||
|
@ -196,4 +196,24 @@ func TestLocalComposeRun(t *testing.T) {
|
||||
"front", "env")
|
||||
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
|
||||
})
|
||||
|
||||
t.Run("compose run -rm with stop signal", func(t *testing.T) {
|
||||
projectName := "run-test"
|
||||
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "-f", "./fixtures/ps-test/compose.yaml", "run", "--rm", "-d", "nginx")
|
||||
res.Assert(t, icmd.Success)
|
||||
|
||||
res = c.RunDockerCmd(t, "ps", "--quiet", "--filter", "name=run-test-nginx")
|
||||
containerID := strings.TrimSpace(res.Stdout())
|
||||
|
||||
res = c.RunDockerCmd(t, "stop", containerID)
|
||||
res.Assert(t, icmd.Success)
|
||||
res = c.RunDockerCmd(t, "ps", "--all", "--filter", "name=run-test-nginx", "--format", "'{{.Names}}'")
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test-nginx"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("compose run --env", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "--env", "FOO=BAR",
|
||||
"front", "env")
|
||||
res.Assert(t, icmd.Expected{Out: "FOO=BAR"})
|
||||
})
|
||||
}
|
||||
|
@ -37,6 +37,11 @@ func TestUnusedMissingEnvFile(t *testing.T) {
|
||||
defer c.cleanupWithDown(t, "unused_dotenv")
|
||||
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "up", "-d", "serviceA")
|
||||
|
||||
// Runtime operations should work even with missing env file
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "ps")
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "logs")
|
||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/env_file/compose.yaml", "down")
|
||||
}
|
||||
|
||||
func TestRunEnvFile(t *testing.T) {
|
||||
|
@ -3,6 +3,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: bridge
|
||||
name: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
|
@ -3,8 +3,8 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: bridge
|
||||
namespace: bridge
|
||||
name: {{ .Values.projectName }}
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
data:
|
||||
|
@ -10,13 +10,13 @@ metadata:
|
||||
com.docker.compose.service: serviceA
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: {{ .Values.deployment.defaultReplicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
strategy:
|
||||
type: Recreate
|
||||
type: {{ .Values.deployment.strategy }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
@ -28,6 +28,10 @@ spec:
|
||||
- name: servicea
|
||||
image: {{ .Values.serviceA.image }}
|
||||
imagePullPolicy: {{ .Values.serviceA.imagePullPolicy }}
|
||||
resources:
|
||||
limits:
|
||||
cpu: {{ .Values.resources.defaultCpuLimit }}
|
||||
memory: {{ .Values.resources.defaultMemoryLimit }}
|
||||
ports:
|
||||
- name: servicea-8080
|
||||
containerPort: 8080
|
||||
@ -39,7 +43,7 @@ spec:
|
||||
volumes:
|
||||
- name: etc-my-config1-txt
|
||||
configMap:
|
||||
name: bridge
|
||||
name: {{ .Values.projectName }}
|
||||
items:
|
||||
- key: my-config
|
||||
path: my-config
|
||||
|
@ -12,7 +12,7 @@ metadata:
|
||||
com.docker.compose.service: serviceA
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
|
@ -10,13 +10,13 @@ metadata:
|
||||
com.docker.compose.service: serviceB
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: {{ .Values.deployment.defaultReplicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
strategy:
|
||||
type: Recreate
|
||||
type: {{ .Values.deployment.strategy }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
@ -29,6 +29,10 @@ spec:
|
||||
- name: serviceb
|
||||
image: {{ .Values.serviceB.image }}
|
||||
imagePullPolicy: {{ .Values.serviceB.imagePullPolicy }}
|
||||
resources:
|
||||
limits:
|
||||
cpu: {{ .Values.resources.defaultCpuLimit }}
|
||||
memory: {{ .Values.resources.defaultMemoryLimit }}
|
||||
ports:
|
||||
- name: serviceb-8082
|
||||
containerPort: 8082
|
||||
|
@ -10,7 +10,7 @@ metadata:
|
||||
com.docker.compose.service: serviceB
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
|
@ -1,8 +1,25 @@
|
||||
#! values.yaml
|
||||
# Project Name
|
||||
projectName: bridge
|
||||
# Namespace
|
||||
namespace: bridge
|
||||
# Default deployment settings
|
||||
deployment:
|
||||
strategy: Recreate
|
||||
defaultReplicas: 1
|
||||
# Default resource limits
|
||||
resources:
|
||||
defaultCpuLimit: "100m"
|
||||
defaultMemoryLimit: "512Mi"
|
||||
# Service settings
|
||||
service:
|
||||
type: LoadBalancer
|
||||
# Storage settings
|
||||
storage:
|
||||
defaultStorageClass: "hostpath"
|
||||
defaultSize: "100Mi"
|
||||
defaultAccessMode: "ReadWriteOnce"
|
||||
# Services variables
|
||||
|
||||
serviceA:
|
||||
image: alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
49
pkg/e2e/fixtures/build-test/long-output-line/Dockerfile
Normal file
49
pkg/e2e/fixtures/build-test/long-output-line/Dockerfile
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
FROM alpine
|
||||
# We generate warnings *on purpose* to bloat the JSON output of bake
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
||||
ARG AWS_SECRET_ACCESS_KEY=FAKE_TO_GENERATE_WARNING_OUTPUT
|
@ -0,0 +1,5 @@
|
||||
services:
|
||||
long-line:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
19
pkg/e2e/fixtures/build-test/profiles/Dockerfile
Normal file
19
pkg/e2e/fixtures/build-test/profiles/Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
FROM alpine
|
||||
RUN --mount=type=secret,id=test-secret ls -la /run/secrets/; cp /run/secrets/test-secret /tmp
|
||||
|
||||
CMD ["cat", "/tmp/test-secret"]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user