mirror of
https://github.com/docker/compose.git
synced 2026-02-14 12:39:23 +08:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
343117233b | ||
|
|
f599a8cdd2 | ||
|
|
63b06f5563 | ||
|
|
1d34661e91 | ||
|
|
0f9e6ab832 | ||
|
|
15c9651a3a | ||
|
|
4893a8b9ad | ||
|
|
97530790fa | ||
|
|
213c03f99a | ||
|
|
ebd7b761f2 | ||
|
|
ea48480d80 | ||
|
|
8151b59288 | ||
|
|
ec49baca56 | ||
|
|
7b9ad96240 | ||
|
|
9b67a48c33 | ||
|
|
0d0e12cc85 | ||
|
|
92fafccfb2 | ||
|
|
fee8aee8f0 | ||
|
|
40f5786e68 | ||
|
|
61e44da936 | ||
|
|
0bf7d1ea25 | ||
|
|
80ace63dfb | ||
|
|
27e90a3fdf | ||
|
|
3ca75bdf55 | ||
|
|
eb3074bbda | ||
|
|
f4fc010d6b | ||
|
|
693b9ef078 | ||
|
|
046879a4a2 | ||
|
|
7c79b23005 | ||
|
|
ad4cbee498 | ||
|
|
60256a875c | ||
|
|
45bd60c33a | ||
|
|
cf89fd1aa1 | ||
|
|
23fef850b9 | ||
|
|
12b73bea73 | ||
|
|
2e71440bee | ||
|
|
d49a68ecbf | ||
|
|
be83f63f26 | ||
|
|
9a9227ce64 | ||
|
|
024f8ebdc5 | ||
|
|
8c622da20b | ||
|
|
bbb2b76a14 |
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,10 @@ linters:
|
||||
deny:
|
||||
- pkg: io/ioutil
|
||||
desc: io/ioutil package has been deprecated
|
||||
- pkg: golang.org/x/exp/maps
|
||||
desc: use stdlib maps package
|
||||
- pkg: golang.org/x/exp/slices
|
||||
desc: use stdlib slices package
|
||||
- pkg: gopkg.in/yaml.v2
|
||||
desc: compose-go uses yaml.v3
|
||||
gocritic:
|
||||
|
||||
@@ -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.10
|
||||
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
|
||||
|
||||
149
cmd/compose/bridge.go
Normal file
149
cmd/compose/bridge.go
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/bridge"
|
||||
)
|
||||
|
||||
func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "bridge CMD [OPTIONS]",
|
||||
Short: "Convert compose files into another model",
|
||||
TraverseChildren: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
convertCommand(p, dockerCli),
|
||||
transformersCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
convertOpts := bridge.ConvertOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "convert",
|
||||
Short: "Convert compose files to Kubernetes manifests, Helm charts, or another model",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runConvert(ctx, dockerCli, p, convertOpts)
|
||||
}),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&convertOpts.Output, "output", "o", "out", "The output directory for the Kubernetes resources")
|
||||
flags.StringArrayVarP(&convertOpts.Transformations, "transformation", "t", nil, "Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)")
|
||||
flags.StringVar(&convertOpts.Templates, "templates", "", "Directory containing transformation templates")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
|
||||
project, _, err := p.ToProject(ctx, dockerCli, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bridge.Convert(ctx, dockerCli, project, opts)
|
||||
}
|
||||
|
||||
func transformersCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "transformations CMD [OPTIONS]",
|
||||
Short: "Manage transformation images",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
listTransformersCommand(dockerCli),
|
||||
createTransformerCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func listTransformersCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := lsOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List available transformations",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
transformers, err := bridge.ListTransformers(ctx, dockerCli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return displayTransformer(dockerCli, transformers, options)
|
||||
}),
|
||||
}
|
||||
cmd.Flags().StringVar(&options.Format, "format", "table", "Format the output. Values: [table | json]")
|
||||
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display transformer names")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func displayTransformer(dockerCli command.Cli, transformers []image.Summary, options lsOptions) error {
|
||||
if options.Quiet {
|
||||
for _, t := range transformers {
|
||||
if len(t.RepoTags) > 0 {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), t.RepoTags[0])
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), t.ID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return formatter.Print(transformers, options.Format, dockerCli.Out(),
|
||||
func(w io.Writer) {
|
||||
for _, img := range transformers {
|
||||
id := stringid.TruncateID(img.ID)
|
||||
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
|
||||
repo, tag := "<none>", "<none>"
|
||||
if len(img.RepoTags) > 0 {
|
||||
ref, err := reference.ParseDockerRef(img.RepoTags[0])
|
||||
if err == nil {
|
||||
// ParseDockerRef will reject a local image ID
|
||||
repo = reference.FamiliarName(ref)
|
||||
if tagged, ok := ref.(reference.Tagged); ok {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", id, repo, tag, size)
|
||||
}
|
||||
},
|
||||
"IMAGE ID", "REPO", "TAGS", "SIZE")
|
||||
}
|
||||
|
||||
func createTransformerCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts bridge.CreateTransformerOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTION] PATH",
|
||||
Short: "Create a new transformation",
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
opts.Dest = args[0]
|
||||
return bridge.CreateTransformer(ctx, dockerCli, opts)
|
||||
}),
|
||||
}
|
||||
cmd.Flags().StringVarP(&opts.From, "from", "f", "", "Existing transformation to copy (default: docker/compose-bridge-kubernetes)")
|
||||
return cmd
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
buildkit "github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -137,7 +136,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
flags.Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
|
||||
flags.MarkHidden("no-rm") //nolint:errcheck
|
||||
flags.VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.")
|
||||
flags.StringVar(&p.Progress, "progress", string(buildkit.AutoMode), fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
|
||||
flags.StringVar(&p.Progress, "progress", "", fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
|
||||
flags.MarkHidden("progress") //nolint:errcheck
|
||||
flags.BoolVar(&opts.print, "print", false, "Print equivalent bake file")
|
||||
flags.BoolVar(&opts.check, "check", false, "Check build configuration")
|
||||
@@ -146,6 +145,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
|
||||
|
||||
@@ -47,7 +47,6 @@ import (
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/remote"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
buildkit "github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -230,7 +229,7 @@ func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
|
||||
f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
|
||||
f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
|
||||
f.StringVar(&o.Progress, "progress", defaultStringVar(ComposeProgress, string(buildkit.AutoMode)), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
|
||||
f.StringVar(&o.Progress, "progress", os.Getenv(ComposeProgress), fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
|
||||
f.BoolVar(&o.All, "all-resources", false, "Include all resources, even those not used by services")
|
||||
_ = f.MarkHidden("workdir")
|
||||
}
|
||||
@@ -242,14 +241,6 @@ func defaultStringArrayVar(env string) []string {
|
||||
})
|
||||
}
|
||||
|
||||
// get default value for a command line flag from the env variable, if the env variable is not set, it returns the provided default value 'def'
|
||||
func defaultStringVar(env, def string) string {
|
||||
if v, ok := os.LookupEnv(env); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cli, services ...string) (*types.Project, string, error) {
|
||||
name := o.ProjectName
|
||||
var project *types.Project
|
||||
@@ -516,8 +507,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
}
|
||||
|
||||
switch opts.Progress {
|
||||
case ui.ModeAuto:
|
||||
ui.Mode = ui.ModeAuto
|
||||
case "", ui.ModeAuto:
|
||||
if ansi == "never" {
|
||||
ui.Mode = ui.ModePlain
|
||||
}
|
||||
@@ -645,6 +635,8 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
|
||||
watchCommand(&opts, dockerCli, backend),
|
||||
publishCommand(&opts, dockerCli, backend),
|
||||
alphaCommand(&opts, dockerCli, backend),
|
||||
bridgeCommand(&opts, dockerCli),
|
||||
volumesCommand(&opts, dockerCli, backend),
|
||||
)
|
||||
|
||||
c.Flags().SetInterspersed(false)
|
||||
|
||||
@@ -50,6 +50,7 @@ type configOptions struct {
|
||||
noResolveEnv bool
|
||||
services bool
|
||||
volumes bool
|
||||
networks bool
|
||||
profiles bool
|
||||
images bool
|
||||
hash string
|
||||
@@ -111,6 +112,9 @@ 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.hash != "" {
|
||||
return runHash(ctx, dockerCli, opts)
|
||||
}
|
||||
@@ -147,6 +151,7 @@ func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags.BoolVar(&opts.services, "services", false, "Print the service names, one per line.")
|
||||
flags.BoolVar(&opts.volumes, "volumes", false, "Print the volume names, one per line.")
|
||||
flags.BoolVar(&opts.networks, "networks", false, "Print the network names, one per line.")
|
||||
flags.BoolVar(&opts.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 +372,17 @@ 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 runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
|
||||
var services []string
|
||||
if opts.hash != "*" {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -20,9 +20,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
@@ -30,7 +33,6 @@ import (
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
type imageOptions struct {
|
||||
@@ -76,7 +78,7 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
if i := strings.IndexRune(img.ID, ':'); i >= 0 {
|
||||
id = id[i+1:]
|
||||
}
|
||||
if !utils.StringContains(ids, id) {
|
||||
if !slices.Contains(ids, id) {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
@@ -85,14 +87,42 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if opts.Format == "json" {
|
||||
|
||||
sort.Slice(images, func(i, j int) bool {
|
||||
return images[i].ContainerName < images[j].ContainerName
|
||||
})
|
||||
type img struct {
|
||||
ID string `json:"ID"`
|
||||
ContainerName string `json:"ContainerName"`
|
||||
Repository string `json:"Repository"`
|
||||
Tag string `json:"Tag"`
|
||||
Platform string `json:"Platform"`
|
||||
Size int64 `json:"Size"`
|
||||
LastTagTime time.Time `json:"LastTagTime"`
|
||||
}
|
||||
// Convert map to slice
|
||||
var imageList []img
|
||||
for ctr, i := range images {
|
||||
imageList = append(imageList, img{
|
||||
ContainerName: ctr,
|
||||
ID: i.ID,
|
||||
Repository: i.Repository,
|
||||
Tag: i.Tag,
|
||||
Platform: platforms.Format(i.Platform),
|
||||
Size: i.Size,
|
||||
LastTagTime: i.LastTagTime,
|
||||
})
|
||||
}
|
||||
json, err := formatter.ToJSON(imageList, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintln(dockerCli.Out(), json)
|
||||
return err
|
||||
}
|
||||
|
||||
return formatter.Print(images, opts.Format, dockerCli.Out(),
|
||||
func(w io.Writer) {
|
||||
for _, img := range images {
|
||||
for _, container := range slices.Sorted(maps.Keys(images)) {
|
||||
img := images[container]
|
||||
id := stringid.TruncateID(img.ID)
|
||||
size := units.HumanSizeWithPrecision(float64(img.Size), 3)
|
||||
repo := img.Repository
|
||||
@@ -103,8 +133,10 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
|
||||
if tag == "" {
|
||||
tag = "<none>"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", img.ContainerName, repo, tag, id, size)
|
||||
created := units.HumanDuration(time.Now().UTC().Sub(img.LastTagTime)) + " ago"
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
container, repo, tag, platforms.Format(img.Platform), id, size, created)
|
||||
}
|
||||
},
|
||||
"CONTAINER", "REPOSITORY", "TAG", "IMAGE ID", "SIZE")
|
||||
"CONTAINER", "REPOSITORY", "TAG", "PLATFORM", "IMAGE ID", "SIZE", "CREATED")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@@ -32,7 +33,6 @@ import (
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
ui "github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/prompt"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
|
||||
@@ -44,7 +44,7 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
|
||||
|
||||
// default platform only applies if the service doesn't specify
|
||||
if defaultPlatform != "" && service.Platform == "" {
|
||||
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, defaultPlatform) {
|
||||
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, defaultPlatform) {
|
||||
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, defaultPlatform)
|
||||
}
|
||||
service.Platform = defaultPlatform
|
||||
@@ -52,7 +52,7 @@ func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
|
||||
|
||||
if service.Platform != "" {
|
||||
if len(service.Build.Platforms) > 0 {
|
||||
if !utils.StringContains(service.Build.Platforms, service.Platform) {
|
||||
if !slices.Contains(service.Build.Platforms, service.Platform) {
|
||||
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliformatter "github.com/docker/cli/cli/command/formatter"
|
||||
@@ -101,7 +101,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
|
||||
names := project.ServiceNames()
|
||||
if len(services) > 0 {
|
||||
for _, service := range services {
|
||||
if !utils.StringContains(names, service) {
|
||||
if !slices.Contains(names, service) {
|
||||
return fmt.Errorf("no such service: %s", service)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv
|
||||
services := []string{}
|
||||
for _, c := range containers {
|
||||
s := c.Service
|
||||
if !utils.StringContains(services, s) {
|
||||
if !slices.Contains(services, s) {
|
||||
services = append(services, s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/morikuni/aec"
|
||||
@@ -97,7 +98,7 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
|
||||
}
|
||||
|
||||
func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pullOptions, services []string) error {
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, services)
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -324,15 +311,14 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
Interactive: options.interactive,
|
||||
WorkingDir: options.workdir,
|
||||
User: options.user,
|
||||
CapAdd: options.capAdd.GetAll(),
|
||||
CapDrop: options.capDrop.GetAll(),
|
||||
CapAdd: options.capAdd.GetSlice(),
|
||||
CapDrop: options.capDrop.GetSlice(),
|
||||
Environment: environment.Values(),
|
||||
Entrypoint: options.entrypointCmd,
|
||||
Labels: labels,
|
||||
UseNetworkAliases: options.useAliases,
|
||||
NoDeps: options.noDeps,
|
||||
Index: 0,
|
||||
QuietPull: options.quietPull,
|
||||
}
|
||||
|
||||
for name, service := range project.Services {
|
||||
@@ -352,34 +338,3 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func startDependencies(ctx context.Context, backend api.Service, project types.Project, buildOpts *api.BuildOptions, options runOptions) error {
|
||||
dependencies := types.Services{}
|
||||
var requestedService types.ServiceConfig
|
||||
for name, service := range project.Services {
|
||||
if name != options.Service {
|
||||
dependencies[name] = service
|
||||
} else {
|
||||
requestedService = service
|
||||
}
|
||||
}
|
||||
|
||||
project.Services = dependencies
|
||||
project.DisabledServices[options.Service] = requestedService
|
||||
err := backend.Create(ctx, &project, api.CreateOptions{
|
||||
Build: buildOpts,
|
||||
IgnoreOrphans: options.ignoreOrphans,
|
||||
RemoveOrphans: options.removeOrphans,
|
||||
QuietPull: options.quietPull,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(dependencies) > 0 {
|
||||
return backend.Start(ctx, project.Name, api.StartOptions{
|
||||
Project: &project,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,14 +19,13 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"golang.org/x/exp/maps"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -60,7 +59,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
}
|
||||
|
||||
func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error {
|
||||
services := maps.Keys(serviceReplicaTuples)
|
||||
services := slices.Sorted(maps.Keys(serviceReplicaTuples))
|
||||
project, _, err := opts.ToProject(ctx, dockerCli, services)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -261,6 +261,7 @@ func runUp(
|
||||
return err
|
||||
}
|
||||
bo.Services = services
|
||||
bo.Deps = !upOptions.noDeps
|
||||
build = &bo
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -117,9 +117,10 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
|
||||
}
|
||||
|
||||
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false)
|
||||
return backend.Watch(ctx, project, services, api.WatchOptions{
|
||||
Build: &build,
|
||||
LogTo: consumer,
|
||||
Prune: watchOpts.prune,
|
||||
return backend.Watch(ctx, project, api.WatchOptions{
|
||||
Build: &build,
|
||||
LogTo: consumer,
|
||||
Prune: watchOpts.prune,
|
||||
Services: services,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,10 +118,6 @@ 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") {
|
||||
@@ -131,10 +127,6 @@ func (l *logConsumer) write(w io.Writer, container, message string) {
|
||||
_, _ = fmt.Fprintf(w, "%s%s\n", p.prefix, line)
|
||||
}
|
||||
}
|
||||
|
||||
if KeyboardManager != nil {
|
||||
KeyboardManager.PrintKeyboardInfo()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logConsumer) Status(container, msg string) {
|
||||
@@ -168,3 +160,31 @@ 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()
|
||||
}
|
||||
|
||||
func (l logDecorator) Register(container string) {
|
||||
l.decorated.Register(container)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/watch"
|
||||
"github.com/eiannone/keyboard"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
)
|
||||
|
||||
@@ -71,26 +69,14 @@ func (ke *KeyboardError) error() string {
|
||||
}
|
||||
|
||||
type KeyboardWatch struct {
|
||||
Watcher watch.Notify
|
||||
Watching bool
|
||||
WatchFn func(ctx context.Context, doneCh chan bool, project *types.Project, services []string, options api.WatchOptions) error
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
Watcher Feature
|
||||
}
|
||||
|
||||
func (kw *KeyboardWatch) isWatching() bool {
|
||||
return kw.Watching
|
||||
}
|
||||
|
||||
func (kw *KeyboardWatch) switchWatching() {
|
||||
kw.Watching = !kw.Watching
|
||||
}
|
||||
|
||||
func (kw *KeyboardWatch) newContext(ctx context.Context) context.CancelFunc {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
kw.Ctx = ctx
|
||||
kw.Cancel = cancel
|
||||
return cancel
|
||||
// Feature is an compose feature that can be started/stopped by a menu command
|
||||
type Feature interface {
|
||||
Start(context.Context) error
|
||||
Stop() error
|
||||
}
|
||||
|
||||
type KEYBOARD_LOG_LEVEL int
|
||||
@@ -103,42 +89,26 @@ const (
|
||||
|
||||
type LogKeyboard struct {
|
||||
kError KeyboardError
|
||||
Watch KeyboardWatch
|
||||
Watch *KeyboardWatch
|
||||
IsDockerDesktopActive bool
|
||||
IsWatchConfigured bool
|
||||
logLevel KEYBOARD_LOG_LEVEL
|
||||
signalChannel chan<- os.Signal
|
||||
}
|
||||
|
||||
var (
|
||||
KeyboardManager *LogKeyboard
|
||||
eg multierror.Group
|
||||
)
|
||||
|
||||
func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured bool,
|
||||
sc chan<- os.Signal,
|
||||
watchFn func(ctx context.Context,
|
||||
doneCh chan bool,
|
||||
project *types.Project,
|
||||
services []string,
|
||||
options api.WatchOptions,
|
||||
) error,
|
||||
) {
|
||||
km := LogKeyboard{}
|
||||
km.IsDockerDesktopActive = isDockerDesktopActive
|
||||
km.IsWatchConfigured = isWatchConfigured
|
||||
km.logLevel = INFO
|
||||
|
||||
km.Watch.Watching = false
|
||||
km.Watch.WatchFn = watchFn
|
||||
|
||||
km.signalChannel = sc
|
||||
|
||||
KeyboardManager = &km
|
||||
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal) *LogKeyboard {
|
||||
return &LogKeyboard{
|
||||
IsDockerDesktopActive: isDockerDesktopActive,
|
||||
logLevel: INFO,
|
||||
signalChannel: sc,
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -209,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")
|
||||
@@ -233,48 +203,51 @@ func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Pro
|
||||
if !lk.IsDockerDesktopActive {
|
||||
return
|
||||
}
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop")
|
||||
lk.keyboardError("View", err)
|
||||
}
|
||||
return err
|
||||
}),
|
||||
)
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop")
|
||||
lk.keyboardError("View", err)
|
||||
}
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
|
||||
if !lk.IsDockerDesktopActive {
|
||||
return
|
||||
}
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop Compose UI")
|
||||
lk.keyboardError("View Config", err)
|
||||
}
|
||||
return err
|
||||
}),
|
||||
)
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop Compose UI")
|
||||
lk.keyboardError("View Config", err)
|
||||
}
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Project) {
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop Compose UI")
|
||||
lk.keyboardError("Watch Docs", err)
|
||||
}
|
||||
return err
|
||||
}),
|
||||
)
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
|
||||
err := open.Run(link)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not open Docker Desktop Compose UI")
|
||||
lk.keyboardError("Watch Docs", err)
|
||||
}
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) keyboardError(prefix string, err error) {
|
||||
@@ -288,58 +261,54 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
|
||||
if !lk.IsWatchConfigured {
|
||||
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
|
||||
if lk.Watch == nil {
|
||||
return
|
||||
}
|
||||
lk.Watch.switchWatching()
|
||||
if !lk.Watch.isWatching() {
|
||||
lk.Watch.Cancel()
|
||||
if lk.Watch.Watching {
|
||||
err := lk.Watch.Watcher.Stop()
|
||||
if err != nil {
|
||||
options.Start.Attach.Err(api.WatchLogger, err.Error())
|
||||
} else {
|
||||
lk.Watch.Watching = false
|
||||
}
|
||||
} else {
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
if options.Create.Build == nil {
|
||||
err := fmt.Errorf("cannot run watch mode with flag --no-build")
|
||||
lk.keyboardError("Watch", err)
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
err := lk.Watch.Watcher.Start(ctx)
|
||||
if err != nil {
|
||||
options.Start.Attach.Err(api.WatchLogger, err.Error())
|
||||
} else {
|
||||
lk.Watch.Watching = true
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
lk.Watch.newContext(ctx)
|
||||
buildOpts := *options.Create.Build
|
||||
buildOpts.Quiet = true
|
||||
err := lk.Watch.WatchFn(lk.Watch.Ctx, doneCh, project, options.Start.Services, api.WatchOptions{
|
||||
Build: &buildOpts,
|
||||
LogTo: options.Start.Attach,
|
||||
})
|
||||
if err != nil {
|
||||
lk.Watch.switchWatching()
|
||||
options.Start.Attach.Err(api.WatchLogger, err.Error())
|
||||
}
|
||||
return err
|
||||
}))
|
||||
})()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
|
||||
func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
|
||||
switch kRune := event.Rune; kRune {
|
||||
case 'v':
|
||||
lk.openDockerDesktop(ctx, project)
|
||||
case 'w':
|
||||
if !lk.IsWatchConfigured {
|
||||
if lk.Watch == nil {
|
||||
// we try to open watch docs if DD is installed
|
||||
if lk.IsDockerDesktopActive {
|
||||
lk.openDDWatchDocs(ctx, project)
|
||||
}
|
||||
// either way we mark menu/watch as an error
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
|
||||
lk.keyboardError("Watch", err)
|
||||
return err
|
||||
}))
|
||||
return
|
||||
go func() {
|
||||
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
|
||||
func(ctx context.Context) error {
|
||||
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
|
||||
lk.keyboardError("Watch", err)
|
||||
return err
|
||||
})()
|
||||
}()
|
||||
}
|
||||
lk.StartWatch(ctx, doneCh, project, options)
|
||||
lk.ToggleWatch(ctx, options)
|
||||
case 'o':
|
||||
lk.openDDComposeUI(ctx, project)
|
||||
}
|
||||
@@ -350,10 +319,6 @@ func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Cont
|
||||
ShowCursor()
|
||||
|
||||
lk.logLevel = NONE
|
||||
if lk.Watch.Watching && lk.Watch.Cancel != nil {
|
||||
lk.Watch.Cancel()
|
||||
_ = eg.Wait().ErrorOrNil() // Need to print this ?
|
||||
}
|
||||
// will notify main thread to kill and will handle gracefully
|
||||
lk.signalChannel <- syscall.SIGINT
|
||||
case keyboard.KeyEnter:
|
||||
@@ -362,6 +327,13 @@ func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Cont
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -37,34 +39,55 @@ 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
|
||||
c.AddCommand(&cobra.Command{
|
||||
Use: "up",
|
||||
Run: up,
|
||||
|
||||
var options options
|
||||
upCmd := &cobra.Command{
|
||||
Use: "up",
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
up(options, args)
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
})
|
||||
c.AddCommand(&cobra.Command{
|
||||
}
|
||||
|
||||
upCmd.Flags().StringVar(&options.db, "type", "", "Database type (mysql, postgres, etc.)")
|
||||
_ = upCmd.MarkFlagRequired("type")
|
||||
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")
|
||||
|
||||
downCmd := &cobra.Command{
|
||||
Use: "down",
|
||||
Run: down,
|
||||
Args: cobra.ExactArgs(1),
|
||||
})
|
||||
}
|
||||
downCmd.Flags().String("name", "", "Name of the database to be deleted")
|
||||
_ = downCmd.MarkFlagRequired("name")
|
||||
|
||||
c.AddCommand(upCmd, downCmd)
|
||||
c.AddCommand(metadataCommand(upCmd, downCmd))
|
||||
return c
|
||||
}
|
||||
|
||||
const lineSeparator = "\n"
|
||||
|
||||
func up(_ *cobra.Command, args []string) {
|
||||
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)
|
||||
}
|
||||
@@ -72,3 +95,58 @@ func up(_ *cobra.Command, args []string) {
|
||||
func down(_ *cobra.Command, _ []string) {
|
||||
fmt.Printf(`{ "type": "error", "message": "Permission error" }%s`, lineSeparator)
|
||||
}
|
||||
|
||||
func metadataCommand(upCmd, downCmd *cobra.Command) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "metadata",
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
metadata(upCmd, downCmd)
|
||||
},
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
}
|
||||
|
||||
func metadata(upCmd, downCmd *cobra.Command) {
|
||||
metadata := ProviderMetadata{}
|
||||
metadata.Description = "Manage services on AwesomeCloud"
|
||||
metadata.Up = commandParameters(upCmd)
|
||||
metadata.Down = commandParameters(downCmd)
|
||||
jsonMetadata, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(jsonMetadata))
|
||||
}
|
||||
|
||||
func commandParameters(cmd *cobra.Command) CommandMetadata {
|
||||
cmdMetadata := CommandMetadata{}
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
_, isRequired := f.Annotations[cobra.BashCompOneRequiredFlag]
|
||||
cmdMetadata.Parameters = append(cmdMetadata.Parameters, Metadata{
|
||||
Name: f.Name,
|
||||
Description: f.Usage,
|
||||
Required: isRequired,
|
||||
Type: f.Value.Type(),
|
||||
Default: f.DefValue,
|
||||
})
|
||||
})
|
||||
return cmdMetadata
|
||||
}
|
||||
|
||||
type ProviderMetadata struct {
|
||||
Description string `json:"description"`
|
||||
Up CommandMetadata `json:"up"`
|
||||
Down CommandMetadata `json:"down"`
|
||||
}
|
||||
|
||||
type CommandMetadata struct {
|
||||
Parameters []Metadata `json:"parameters"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
Type string `json:"type"`
|
||||
Default string `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ the resource(s) needed to run a service.
|
||||
options:
|
||||
type: mysql
|
||||
size: 256
|
||||
name: myAwesomeCloudDB
|
||||
```
|
||||
|
||||
`provider.type` tells Compose the binary to run, which can be either:
|
||||
@@ -104,8 +105,72 @@ into its runtime environment.
|
||||
## Down lifecycle
|
||||
|
||||
`down` lifecycle is equivalent to `up` with the `<provider> compose --project-name <NAME> down <SERVICE>` command.
|
||||
The provider is responsible for releasing all resources associated with the service.
|
||||
The provider is responsible for releasing all resources associated with the service.
|
||||
|
||||
## Provide metadata about options
|
||||
|
||||
Compose extensions *MAY* optionally implement a `metadata` subcommand to provide information about the parameters accepted by the `up` and `down` commands.
|
||||
|
||||
The `metadata` subcommand takes no parameters and returns a JSON structure on the `stdout` channel that describes the parameters accepted by both the `up` and `down` commands, including whether each parameter is mandatory or optional.
|
||||
|
||||
```console
|
||||
awesomecloud compose metadata
|
||||
```
|
||||
|
||||
The expected JSON output format is:
|
||||
```json
|
||||
{
|
||||
"description": "Manage services on AwesomeCloud",
|
||||
"up": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "type",
|
||||
"description": "Database type (mysql, postgres, etc.)",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"description": "Database size in GB",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"default": "10"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Name of the database to be created",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"down": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Name of the database to be removed",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
The top elements are:
|
||||
- `description`: Human-readable description of the provider
|
||||
- `up`: Object describing the parameters accepted by the `up` command
|
||||
- `down`: Object describing the parameters accepted by the `down` command
|
||||
|
||||
And for each command parameter, you should include the following properties:
|
||||
- `name`: The parameter name (without `--` prefix)
|
||||
- `description`: Human-readable description of the parameter
|
||||
- `required`: Boolean indicating if the parameter is mandatory
|
||||
- `type`: Parameter type (`string`, `integer`, `boolean`, etc.)
|
||||
- `default`: Default value (optional, only for non-required parameters)
|
||||
- `enum`: List of possible values supported by the parameter separated by `,` (optional, only for parameters with a limited set of values)
|
||||
|
||||
This metadata allows Compose and other tools to understand the provider's interface and provide better user experience, such as validation, auto-completion, and documentation generation.
|
||||
|
||||
## Examples
|
||||
|
||||
See [example](examples/provider.go) for illustration on implementing this API in a command line
|
||||
See [example](examples/provider.go) for illustration on implementing this API in a command line
|
||||
|
||||
@@ -12,6 +12,7 @@ Define and run multi-container applications with Docker
|
||||
| Name | Description |
|
||||
|:--------------------------------|:----------------------------------------------------------------------------------------|
|
||||
| [`attach`](compose_attach.md) | Attach local standard input, output, and error streams to a service's running container |
|
||||
| [`bridge`](compose_bridge.md) | Convert compose files into another model |
|
||||
| [`build`](compose_build.md) | Build or rebuild services |
|
||||
| [`commit`](compose_commit.md) | Create a new image from a service container's changes |
|
||||
| [`config`](compose_config.md) | Parse, resolve and render compose file in canonical format |
|
||||
@@ -42,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 |
|
||||
|
||||
@@ -58,7 +60,7 @@ Define and run multi-container applications with Docker
|
||||
| `-f`, `--file` | `stringArray` | | Compose configuration files |
|
||||
| `--parallel` | `int` | `-1` | Control max parallelism, -1 for unlimited |
|
||||
| `--profile` | `stringArray` | | Specify a profile to enable |
|
||||
| `--progress` | `string` | `auto` | Set type of progress output (auto, tty, plain, json, quiet) |
|
||||
| `--progress` | `string` | | Set type of progress output (auto, tty, plain, json, quiet) |
|
||||
| `--project-directory` | `string` | | Specify an alternate working directory<br>(default: the path of the, first specified, Compose file) |
|
||||
| `-p`, `--project-name` | `string` | | Project name |
|
||||
|
||||
|
||||
22
docs/reference/compose_bridge.md
Normal file
22
docs/reference/compose_bridge.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# docker compose bridge
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Convert compose files into another model
|
||||
|
||||
### Subcommands
|
||||
|
||||
| Name | Description |
|
||||
|:-------------------------------------------------------|:-----------------------------------------------------------------------------|
|
||||
| [`convert`](compose_bridge_convert.md) | Convert compose files to Kubernetes manifests, Helm charts, or another model |
|
||||
| [`transformations`](compose_bridge_transformations.md) | Manage transformation images |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:------------|:-------|:--------|:--------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
17
docs/reference/compose_bridge_convert.md
Normal file
17
docs/reference/compose_bridge_convert.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# docker compose bridge convert
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Convert compose files to Kubernetes manifests, Helm charts, or another model
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:-------------------------|:--------------|:--------|:-------------------------------------------------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `-o`, `--output` | `string` | `out` | The output directory for the Kubernetes resources |
|
||||
| `--templates` | `string` | | Directory containing transformation templates |
|
||||
| `-t`, `--transformation` | `stringArray` | | Transformation to apply to compose model (default: docker/compose-bridge-kubernetes) |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
22
docs/reference/compose_bridge_transformations.md
Normal file
22
docs/reference/compose_bridge_transformations.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# docker compose bridge transformations
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Manage transformation images
|
||||
|
||||
### Subcommands
|
||||
|
||||
| Name | Description |
|
||||
|:-----------------------------------------------------|:-------------------------------|
|
||||
| [`create`](compose_bridge_transformations_create.md) | Create a new transformation |
|
||||
| [`list`](compose_bridge_transformations_list.md) | List available transformations |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:------------|:-------|:--------|:--------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
15
docs/reference/compose_bridge_transformations_create.md
Normal file
15
docs/reference/compose_bridge_transformations_create.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# docker compose bridge transformations create
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Create a new transformation
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:---------------|:---------|:--------|:----------------------------------------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `-f`, `--from` | `string` | | Existing transformation to copy (default: docker/compose-bridge-kubernetes) |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
20
docs/reference/compose_bridge_transformations_list.md
Normal file
20
docs/reference/compose_bridge_transformations_list.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# docker compose bridge transformations list
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
List available transformations
|
||||
|
||||
### Aliases
|
||||
|
||||
`docker compose bridge transformations list`, `docker compose bridge transformations ls`
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--format` | `string` | `table` | Format the output. Values: [table \| json] |
|
||||
| `-q`, `--quiet` | `bool` | | Only display transformer names |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -15,6 +15,7 @@ the canonical format.
|
||||
| `--hash` | `string` | | Print the service config hash, one per line. |
|
||||
| `--images` | `bool` | | Print the image names, one per line. |
|
||||
| `--lock-image-digests` | `bool` | | Produces an override file with image digests |
|
||||
| `--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 |
|
||||
|
||||
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-->
|
||||
|
||||
@@ -6,6 +6,7 @@ pname: docker
|
||||
plink: docker.yaml
|
||||
cname:
|
||||
- docker compose attach
|
||||
- docker compose bridge
|
||||
- docker compose build
|
||||
- docker compose commit
|
||||
- docker compose config
|
||||
@@ -36,10 +37,12 @@ cname:
|
||||
- docker compose unpause
|
||||
- docker compose up
|
||||
- docker compose version
|
||||
- docker compose volumes
|
||||
- docker compose wait
|
||||
- docker compose watch
|
||||
clink:
|
||||
- docker_compose_attach.yaml
|
||||
- docker_compose_bridge.yaml
|
||||
- docker_compose_build.yaml
|
||||
- docker_compose_commit.yaml
|
||||
- docker_compose_config.yaml
|
||||
@@ -70,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:
|
||||
@@ -167,7 +171,6 @@ options:
|
||||
swarm: false
|
||||
- option: progress
|
||||
value_type: string
|
||||
default_value: auto
|
||||
description: Set type of progress output (auto, tty, plain, json, quiet)
|
||||
deprecated: false
|
||||
hidden: false
|
||||
|
||||
@@ -45,7 +45,7 @@ inherited_options:
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
hidden: true
|
||||
experimental: false
|
||||
experimentalcli: true
|
||||
kubernetes: false
|
||||
|
||||
@@ -58,7 +58,7 @@ inherited_options:
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
hidden: true
|
||||
experimental: false
|
||||
experimentalcli: true
|
||||
kubernetes: false
|
||||
|
||||
@@ -69,7 +69,7 @@ inherited_options:
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
hidden: true
|
||||
experimental: false
|
||||
experimentalcli: true
|
||||
kubernetes: false
|
||||
|
||||
29
docs/reference/docker_compose_bridge.yaml
Normal file
29
docs/reference/docker_compose_bridge.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
command: docker compose bridge
|
||||
short: Convert compose files into another model
|
||||
long: Convert compose files into another model
|
||||
pname: docker compose
|
||||
plink: docker_compose.yaml
|
||||
cname:
|
||||
- docker compose bridge convert
|
||||
- docker compose bridge transformations
|
||||
clink:
|
||||
- docker_compose_bridge_convert.yaml
|
||||
- docker_compose_bridge_transformations.yaml
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Execute command in dry run mode
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
||||
59
docs/reference/docker_compose_bridge_convert.yaml
Normal file
59
docs/reference/docker_compose_bridge_convert.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
command: docker compose bridge convert
|
||||
short: |
|
||||
Convert compose files to Kubernetes manifests, Helm charts, or another model
|
||||
long: |
|
||||
Convert compose files to Kubernetes manifests, Helm charts, or another model
|
||||
usage: docker compose bridge convert
|
||||
pname: docker compose bridge
|
||||
plink: docker_compose_bridge.yaml
|
||||
options:
|
||||
- option: output
|
||||
shorthand: o
|
||||
value_type: string
|
||||
default_value: out
|
||||
description: The output directory for the Kubernetes resources
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: templates
|
||||
value_type: string
|
||||
description: Directory containing transformation templates
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: transformation
|
||||
shorthand: t
|
||||
value_type: stringArray
|
||||
default_value: '[]'
|
||||
description: |
|
||||
Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Execute command in dry run mode
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
||||
29
docs/reference/docker_compose_bridge_transformations.yaml
Normal file
29
docs/reference/docker_compose_bridge_transformations.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
command: docker compose bridge transformations
|
||||
short: Manage transformation images
|
||||
long: Manage transformation images
|
||||
pname: docker compose bridge
|
||||
plink: docker_compose_bridge.yaml
|
||||
cname:
|
||||
- docker compose bridge transformations create
|
||||
- docker compose bridge transformations list
|
||||
clink:
|
||||
- docker_compose_bridge_transformations_create.yaml
|
||||
- docker_compose_bridge_transformations_list.yaml
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Execute command in dry run mode
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
command: docker compose bridge transformations create
|
||||
short: Create a new transformation
|
||||
long: Create a new transformation
|
||||
usage: docker compose bridge transformations create [OPTION] PATH
|
||||
pname: docker compose bridge transformations
|
||||
plink: docker_compose_bridge_transformations.yaml
|
||||
options:
|
||||
- option: from
|
||||
shorthand: f
|
||||
value_type: string
|
||||
description: |
|
||||
Existing transformation to copy (default: docker/compose-bridge-kubernetes)
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Execute command in dry run mode
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
command: docker compose bridge transformations list
|
||||
aliases: docker compose bridge transformations list, docker compose bridge transformations ls
|
||||
short: List available transformations
|
||||
long: List available transformations
|
||||
usage: docker compose bridge transformations list
|
||||
pname: docker compose bridge transformations
|
||||
plink: docker_compose_bridge_transformations.yaml
|
||||
options:
|
||||
- option: format
|
||||
value_type: string
|
||||
default_value: table
|
||||
description: 'Format the output. Values: [table | json]'
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: quiet
|
||||
shorthand: q
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Only display transformer names
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Execute command in dry run mode
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
||||
@@ -118,7 +118,6 @@ options:
|
||||
swarm: false
|
||||
- option: progress
|
||||
value_type: string
|
||||
default_value: auto
|
||||
description: Set type of ui output (auto, tty, plain, json, quiet)
|
||||
deprecated: false
|
||||
hidden: true
|
||||
|
||||
@@ -56,6 +56,16 @@ options:
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
44
go.mod
44
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/docker/compose/v2
|
||||
|
||||
go 1.23.8
|
||||
go 1.23.10
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
@@ -8,15 +8,16 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/compose-spec/compose-go/v2 v2.6.4
|
||||
github.com/containerd/containerd/v2 v2.1.1
|
||||
github.com/compose-spec/compose-go/v2 v2.7.1
|
||||
github.com/containerd/containerd/v2 v2.1.3
|
||||
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.1.1+incompatible
|
||||
github.com/docker/cli-docs-tool v0.9.0
|
||||
github.com/docker/docker v28.1.1+incompatible
|
||||
github.com/docker/buildx v0.25.0
|
||||
github.com/docker/cli v28.3.1+incompatible
|
||||
github.com/docker/cli-docs-tool v0.10.0
|
||||
github.com/docker/docker v28.3.1+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
|
||||
@@ -28,7 +29,7 @@ require (
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/moby/buildkit v0.22.0
|
||||
github.com/moby/buildkit v0.23.2
|
||||
github.com/moby/go-archive v0.1.0
|
||||
github.com/moby/patternmatcher v0.6.0
|
||||
github.com/moby/sys/atomicwriter v0.1.0
|
||||
@@ -42,7 +43,6 @@ require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/theupdateframework/notary v0.7.0
|
||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
@@ -53,10 +53,9 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.uber.org/mock v0.5.2
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/sys v0.33.0
|
||||
google.golang.org/grpc v1.72.1
|
||||
google.golang.org/grpc v1.73.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
tags.cncf.io/container-device-interface v1.0.1
|
||||
@@ -83,10 +82,9 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/containerd/containerd/api v1.9.0 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
@@ -105,7 +103,7 @@ require (
|
||||
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
|
||||
@@ -119,7 +117,7 @@ require (
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.5.0 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
@@ -159,12 +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
|
||||
@@ -187,8 +187,8 @@ require (
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
@@ -205,7 +205,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
|
||||
|
||||
86
go.sum
86
go.sum
@@ -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.7.1 h1:EUIbuaD0R/J1KA+FbJMNbcS9+jt/CVudbp5iHqUllSs=
|
||||
github.com/compose-spec/compose-go/v2 v2.7.1/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU=
|
||||
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
|
||||
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
|
||||
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
||||
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/containerd/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.3 h1:eMD2SLcIQPdMlnlNF6fatlrlRLAeDaiGPGwmRKLZKNs=
|
||||
github.com/containerd/containerd/v2 v2.1.3/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=
|
||||
@@ -125,17 +125,19 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/buildx v0.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.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
|
||||
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.9.0 h1:CVwQbE+ZziwlPqrJ7LRyUF6GvCA+6gj7MTCsayaK9t0=
|
||||
github.com/docker/cli-docs-tool v0.9.0/go.mod h1:ClrwlNW+UioiRyH9GiAOe1o3J/TsY3Tr1ipoypjAUtc=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/buildx v0.25.0 h1:qs5WxBo0wQKSXcQ+v6UhWaeM2Pu+95ZCymaimRzInaE=
|
||||
github.com/docker/buildx v0.25.0/go.mod h1:xJcOeBhz49tgqN174MMGuOU4bxNmgfaLnZn7Gm641EE=
|
||||
github.com/docker/cli v28.3.1+incompatible h1:ZUdwOLDEBoE3TE5rdC9IXGY5HPHksJK3M+hJEWhh2mc=
|
||||
github.com/docker/cli v28.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
|
||||
github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
|
||||
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY=
|
||||
github.com/docker/docker v28.3.1+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=
|
||||
@@ -186,8 +188,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 +218,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-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 +245,8 @@ github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/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 +317,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z
|
||||
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/buildkit v0.22.0 h1:aWN06w1YGSVN1XfeZbj2ZbgY+zi5xDAjEFI8Cy9fTjA=
|
||||
github.com/moby/buildkit v0.22.0/go.mod h1:j4pP5hxiTWcz7xuTK2cyxQislHl/N2WWHzOy43DlLJw=
|
||||
github.com/moby/buildkit v0.23.2 h1:gt/dkfcpgTXKx+B9I310kV767hhVqTvEyxGgI3mqsGQ=
|
||||
github.com/moby/buildkit v0.23.2/go.mod h1:iEjAfPQKIuO+8y6OcInInvzqTMiKMbb2RdJz1K/95a0=
|
||||
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 +425,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 +440,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
|
||||
github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
|
||||
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
|
||||
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
|
||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk=
|
||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
@@ -472,10 +476,10 @@ github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8
|
||||
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-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=
|
||||
@@ -544,8 +548,6 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -571,8 +573,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -620,13 +622,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
pusherrors "github.com/containerd/containerd/v2/core/remotes/errors"
|
||||
@@ -104,7 +105,9 @@ func PushManifest(
|
||||
) error {
|
||||
// Check if we need an extra empty layer for the manifest config
|
||||
if ociVersion == api.OCIVersion1_1 || ociVersion == "" {
|
||||
layers = append(layers, Pushable{Descriptor: v1.DescriptorEmptyJSON, Data: []byte("{}")})
|
||||
if err := resolver.Push(ctx, named, v1.DescriptorEmptyJSON, v1.DescriptorEmptyJSON.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// prepare to push the manifest by pushing the layers
|
||||
layerDescriptors := make([]v1.Descriptor, len(layers))
|
||||
@@ -157,14 +160,7 @@ func isNonAuthClientError(statusCode int) bool {
|
||||
// not a client error
|
||||
return false
|
||||
}
|
||||
for _, v := range clientAuthStatusCodes {
|
||||
if statusCode == v {
|
||||
// client auth error
|
||||
return false
|
||||
}
|
||||
}
|
||||
// any other 4xx client error
|
||||
return true
|
||||
return !slices.Contains(clientAuthStatusCodes, statusCode)
|
||||
}
|
||||
|
||||
func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pushable, error) {
|
||||
|
||||
@@ -229,7 +229,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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -19,12 +19,14 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
)
|
||||
|
||||
// Service manages a compose project
|
||||
@@ -78,13 +80,13 @@ type Service interface {
|
||||
// Publish executes the equivalent to a `compose publish`
|
||||
Publish(ctx context.Context, project *types.Project, repository string, options PublishOptions) error
|
||||
// Images executes the equivalent of a `compose images`
|
||||
Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
|
||||
Images(ctx context.Context, projectName string, options ImagesOptions) (map[string]ImageSummary, error)
|
||||
// MaxConcurrency defines upper limit for concurrent operations against engine API
|
||||
MaxConcurrency(parallel int)
|
||||
// DryRunMode defines if dry run applies to the command
|
||||
DryRunMode(ctx context.Context, dryRun bool) (context.Context, error)
|
||||
// Watch services' development context and sync/notify/rebuild/restart on changes
|
||||
Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
|
||||
Watch(ctx context.Context, project *types.Project, options WatchOptions) error
|
||||
// Viz generates a graphviz graph of the project services
|
||||
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
|
||||
// Wait blocks until at least one of the services' container exits
|
||||
@@ -97,8 +99,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
|
||||
}
|
||||
@@ -126,9 +136,10 @@ const WatchLogger = "#watch"
|
||||
|
||||
// WatchOptions group options of the Watch API
|
||||
type WatchOptions struct {
|
||||
Build *BuildOptions
|
||||
LogTo LogConsumer
|
||||
Prune bool
|
||||
Build *BuildOptions
|
||||
LogTo LogConsumer
|
||||
Prune bool
|
||||
Services []string
|
||||
}
|
||||
|
||||
// BuildOptions group options of the Build API
|
||||
@@ -175,13 +186,13 @@ func (o BuildOptions) Apply(project *types.Project) error {
|
||||
continue
|
||||
}
|
||||
if platform != "" {
|
||||
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, platform) {
|
||||
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, platform) {
|
||||
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", name, platform)
|
||||
}
|
||||
service.Platform = platform
|
||||
}
|
||||
if service.Platform != "" {
|
||||
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
|
||||
if len(service.Build.Platforms) > 0 && !slices.Contains(service.Build.Platforms, service.Platform) {
|
||||
return fmt.Errorf("service %q build configuration does not support platform: %s", name, service.Platform)
|
||||
}
|
||||
}
|
||||
@@ -349,7 +360,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
|
||||
@@ -369,8 +380,6 @@ type RunOptions struct {
|
||||
Privileged bool
|
||||
UseNetworkAliases bool
|
||||
NoDeps bool
|
||||
// QuietPull makes the pulling process quiet
|
||||
QuietPull bool
|
||||
// used by exec
|
||||
Index int
|
||||
}
|
||||
@@ -535,12 +544,12 @@ type ContainerProcSummary struct {
|
||||
|
||||
// ImageSummary holds container image description
|
||||
type ImageSummary struct {
|
||||
ID string
|
||||
ContainerName string
|
||||
Repository string
|
||||
Tag string
|
||||
Size int64
|
||||
LastTagTime time.Time
|
||||
ID string
|
||||
Repository string
|
||||
Tag string
|
||||
Platform platforms.Platform
|
||||
Size int64
|
||||
LastTagTime time.Time
|
||||
}
|
||||
|
||||
// ServiceStatus hold status about a service
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/docker/buildx/util/imagetools"
|
||||
"github.com/docker/cli/cli/command"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
@@ -205,18 +206,18 @@ func (d *DryRunClient) CopyToContainer(ctx context.Context, container, path stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DryRunClient) ImageBuild(ctx context.Context, reader io.Reader, options moby.ImageBuildOptions) (moby.ImageBuildResponse, error) {
|
||||
func (d *DryRunClient) ImageBuild(ctx context.Context, reader io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) {
|
||||
jsonMessage, err := json.Marshal(&jsonmessage.JSONMessage{
|
||||
Status: fmt.Sprintf("%[1]sSuccessfully built: dryRunID\n%[1]sSuccessfully tagged: %[2]s\n", DRYRUN_PREFIX, options.Tags[0]),
|
||||
Progress: &jsonmessage.JSONProgress{},
|
||||
ID: "",
|
||||
})
|
||||
if err != nil {
|
||||
return moby.ImageBuildResponse{}, err
|
||||
return build.ImageBuildResponse{}, err
|
||||
}
|
||||
rc := io.NopCloser(bytes.NewReader(jsonMessage))
|
||||
|
||||
return moby.ImageBuildResponse{
|
||||
return build.ImageBuildResponse{
|
||||
Body: rc,
|
||||
OSType: "",
|
||||
}, nil
|
||||
@@ -334,11 +335,11 @@ func (d *DryRunClient) ContainerExecStart(ctx context.Context, execID string, co
|
||||
|
||||
// Functions delegated to original APIClient (not used by Compose or not modifying the Compose stack
|
||||
|
||||
func (d *DryRunClient) ConfigList(ctx context.Context, options moby.ConfigListOptions) ([]swarm.Config, error) {
|
||||
func (d *DryRunClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return d.apiClient.ConfigList(ctx, options)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (moby.ConfigCreateResponse, error) {
|
||||
func (d *DryRunClient) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
return d.apiClient.ConfigCreate(ctx, config)
|
||||
}
|
||||
|
||||
@@ -422,7 +423,7 @@ func (d *DryRunClient) DistributionInspect(ctx context.Context, imageName, encod
|
||||
return d.apiClient.DistributionInspect(ctx, imageName, encodedRegistryAuth)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) BuildCachePrune(ctx context.Context, opts moby.BuildCachePruneOptions) (*moby.BuildCachePruneReport, error) {
|
||||
func (d *DryRunClient) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||
return d.apiClient.BuildCachePrune(ctx, opts)
|
||||
}
|
||||
|
||||
@@ -470,11 +471,11 @@ func (d *DryRunClient) NodeInspectWithRaw(ctx context.Context, nodeID string) (s
|
||||
return d.apiClient.NodeInspectWithRaw(ctx, nodeID)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) NodeList(ctx context.Context, options moby.NodeListOptions) ([]swarm.Node, error) {
|
||||
func (d *DryRunClient) NodeList(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) {
|
||||
return d.apiClient.NodeList(ctx, options)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) NodeRemove(ctx context.Context, nodeID string, options moby.NodeRemoveOptions) error {
|
||||
func (d *DryRunClient) NodeRemove(ctx context.Context, nodeID string, options swarm.NodeRemoveOptions) error {
|
||||
return d.apiClient.NodeRemove(ctx, nodeID, options)
|
||||
}
|
||||
|
||||
@@ -538,15 +539,15 @@ func (d *DryRunClient) PluginCreate(ctx context.Context, createContext io.Reader
|
||||
return d.apiClient.PluginCreate(ctx, createContext, options)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options moby.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {
|
||||
func (d *DryRunClient) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options swarm.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {
|
||||
return d.apiClient.ServiceCreate(ctx, service, options)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options moby.ServiceInspectOptions) (swarm.Service, []byte, error) {
|
||||
func (d *DryRunClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) {
|
||||
return d.apiClient.ServiceInspectWithRaw(ctx, serviceID, options)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) ServiceList(ctx context.Context, options moby.ServiceListOptions) ([]swarm.Service, error) {
|
||||
func (d *DryRunClient) ServiceList(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return d.apiClient.ServiceList(ctx, options)
|
||||
}
|
||||
|
||||
@@ -554,7 +555,7 @@ func (d *DryRunClient) ServiceRemove(ctx context.Context, serviceID string) erro
|
||||
return d.apiClient.ServiceRemove(ctx, serviceID)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options moby.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
|
||||
func (d *DryRunClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
|
||||
return d.apiClient.ServiceUpdate(ctx, serviceID, version, service, options)
|
||||
}
|
||||
|
||||
@@ -570,7 +571,7 @@ func (d *DryRunClient) TaskInspectWithRaw(ctx context.Context, taskID string) (s
|
||||
return d.apiClient.TaskInspectWithRaw(ctx, taskID)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) TaskList(ctx context.Context, options moby.TaskListOptions) ([]swarm.Task, error) {
|
||||
func (d *DryRunClient) TaskList(ctx context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) {
|
||||
return d.apiClient.TaskList(ctx, options)
|
||||
}
|
||||
|
||||
@@ -582,7 +583,7 @@ func (d *DryRunClient) SwarmJoin(ctx context.Context, req swarm.JoinRequest) err
|
||||
return d.apiClient.SwarmJoin(ctx, req)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) SwarmGetUnlockKey(ctx context.Context) (moby.SwarmUnlockKeyResponse, error) {
|
||||
func (d *DryRunClient) SwarmGetUnlockKey(ctx context.Context) (swarm.UnlockKeyResponse, error) {
|
||||
return d.apiClient.SwarmGetUnlockKey(ctx)
|
||||
}
|
||||
|
||||
@@ -602,11 +603,11 @@ func (d *DryRunClient) SwarmUpdate(ctx context.Context, version swarm.Version, s
|
||||
return d.apiClient.SwarmUpdate(ctx, version, swarmSpec, flags)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) SecretList(ctx context.Context, options moby.SecretListOptions) ([]swarm.Secret, error) {
|
||||
func (d *DryRunClient) SecretList(ctx context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) {
|
||||
return d.apiClient.SecretList(ctx, options)
|
||||
}
|
||||
|
||||
func (d *DryRunClient) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (moby.SecretCreateResponse, error) {
|
||||
func (d *DryRunClient) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error) {
|
||||
return d.apiClient.SecretCreate(ctx, secret)
|
||||
}
|
||||
|
||||
|
||||
218
pkg/bridge/convert.go
Normal file
218
pkg/bridge/convert.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 bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli/command"
|
||||
cli "github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ConvertOptions struct {
|
||||
Output string
|
||||
Templates string
|
||||
Transformations []string
|
||||
}
|
||||
|
||||
func Convert(ctx context.Context, dockerCli command.Cli, project *types.Project, opts ConvertOptions) error {
|
||||
if len(opts.Transformations) == 0 {
|
||||
opts.Transformations = []string{DefaultTransformerImage}
|
||||
}
|
||||
// Load image references, secrets and configs, also expose ports
|
||||
project, err := LoadAdditionalResources(ctx, dockerCli, project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// for user to rely on compose.yaml attribute names, not go struct ones, we marshall back into YAML
|
||||
raw, err := project.MarshalYAML(types.WithSecretContent)
|
||||
// Marshall to YAML
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot render project into yaml: %w", err)
|
||||
}
|
||||
var model map[string]any
|
||||
err = yaml.Unmarshal(raw, &model)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot render project into yaml: %w", err)
|
||||
}
|
||||
|
||||
if opts.Output != "" {
|
||||
_ = os.RemoveAll(opts.Output)
|
||||
err := os.MkdirAll(opts.Output, 0o744)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("cannot create output folder: %w", err)
|
||||
}
|
||||
}
|
||||
// Run Transformers images
|
||||
return convert(ctx, dockerCli, model, opts)
|
||||
}
|
||||
|
||||
func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, opts ConvertOptions) error {
|
||||
raw, err := yaml.Marshal(model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := os.TempDir()
|
||||
composeYaml := filepath.Join(dir, "compose.yaml")
|
||||
err = os.WriteFile(composeYaml, raw, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := filepath.Abs(opts.Output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
binds := []string{
|
||||
fmt.Sprintf("%s:%s", dir, "/in"),
|
||||
fmt.Sprintf("%s:%s", out, "/out"),
|
||||
}
|
||||
if opts.Templates != "" {
|
||||
templateDir, err := filepath.Abs(opts.Templates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
binds = append(binds, fmt.Sprintf("%s:%s", templateDir, "/templates"))
|
||||
}
|
||||
|
||||
for _, transformation := range opts.Transformations {
|
||||
_, err = inspectWithPull(ctx, dockerCli, transformation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
|
||||
Image: transformation,
|
||||
Env: []string{"LICENSE_AGREEMENT=true"},
|
||||
User: usr.Uid,
|
||||
}, &container.HostConfig{
|
||||
AutoRemove: true,
|
||||
Binds: binds,
|
||||
}, &network.NetworkingConfig{}, nil, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cli.RunStart(ctx, dockerCli, &cli.StartOptions{
|
||||
Attach: true,
|
||||
Containers: []string{created.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAdditionalResources loads additional resources from the project, such as image references, secrets, configs and exposed ports
|
||||
func LoadAdditionalResources(ctx context.Context, dockerCLI command.Cli, project *types.Project) (*types.Project, error) {
|
||||
for name, service := range project.Services {
|
||||
imageName := api.GetImageNameOrDefault(service, project.Name)
|
||||
|
||||
inspect, err := inspectWithPull(ctx, dockerCLI, imageName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.Image = imageName
|
||||
exposed := utils.Set[string]{}
|
||||
exposed.AddAll(service.Expose...)
|
||||
for port := range inspect.Config.ExposedPorts {
|
||||
exposed.Add(nat.Port(port).Port())
|
||||
}
|
||||
for _, port := range service.Ports {
|
||||
exposed.Add(strconv.Itoa(int(port.Target)))
|
||||
}
|
||||
service.Expose = exposed.Elements()
|
||||
project.Services[name] = service
|
||||
}
|
||||
|
||||
for name, secret := range project.Secrets {
|
||||
f, err := loadFileObject(types.FileObjectConfig(secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
project.Secrets[name] = types.SecretConfig(f)
|
||||
}
|
||||
|
||||
for name, config := range project.Configs {
|
||||
f, err := loadFileObject(types.FileObjectConfig(config))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
project.Configs[name] = types.ConfigObjConfig(f)
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func loadFileObject(conf types.FileObjectConfig) (types.FileObjectConfig, error) {
|
||||
if !conf.External {
|
||||
switch {
|
||||
case conf.Environment != "":
|
||||
conf.Content = os.Getenv(conf.Environment)
|
||||
case conf.File != "":
|
||||
bytes, err := os.ReadFile(conf.File)
|
||||
if err != nil {
|
||||
return conf, err
|
||||
}
|
||||
conf.Content = string(bytes)
|
||||
}
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func inspectWithPull(ctx context.Context, dockerCli command.Cli, imageName string) (image.InspectResponse, error) {
|
||||
inspect, err := dockerCli.Client().ImageInspect(ctx, imageName)
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
var stream io.ReadCloser
|
||||
stream, err = dockerCli.Client().ImagePull(ctx, imageName, image.PullOptions{})
|
||||
if err != nil {
|
||||
return image.InspectResponse{}, err
|
||||
}
|
||||
defer func() { _ = stream.Close() }()
|
||||
|
||||
err = jsonmessage.DisplayJSONMessagesToStream(stream, dockerCli.Out(), nil)
|
||||
if err != nil {
|
||||
return image.InspectResponse{}, err
|
||||
}
|
||||
if inspect, err = dockerCli.Client().ImageInspect(ctx, imageName); err != nil {
|
||||
return image.InspectResponse{}, err
|
||||
}
|
||||
}
|
||||
return inspect, err
|
||||
}
|
||||
118
pkg/bridge/transformers.go
Normal file
118
pkg/bridge/transformers.go
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/moby/go-archive"
|
||||
)
|
||||
|
||||
const (
|
||||
TransformerLabel = "com.docker.compose.bridge"
|
||||
DefaultTransformerImage = "docker/compose-bridge-kubernetes"
|
||||
)
|
||||
|
||||
type CreateTransformerOptions struct {
|
||||
Dest string
|
||||
From string
|
||||
}
|
||||
|
||||
func CreateTransformer(ctx context.Context, dockerCli command.Cli, options CreateTransformerOptions) error {
|
||||
if options.From == "" {
|
||||
options.From = DefaultTransformerImage
|
||||
}
|
||||
out, err := filepath.Abs(options.Dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(out); err == nil {
|
||||
return fmt.Errorf("output folder %s already exists", out)
|
||||
}
|
||||
|
||||
tmpl := filepath.Join(out, "templates")
|
||||
err = os.MkdirAll(tmpl, 0o744)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("cannot create output folder: %w", err)
|
||||
}
|
||||
|
||||
if err := command.ValidateOutputPath(out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
|
||||
Image: options.From,
|
||||
}, &container.HostConfig{}, &network.NetworkingConfig{}, nil, "")
|
||||
defer func() {
|
||||
_ = dockerCli.Client().ContainerRemove(context.Background(), created.ID, container.RemoveOptions{Force: true})
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, "/templates")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = content.Close()
|
||||
}()
|
||||
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: "/templates",
|
||||
Exists: true,
|
||||
IsDir: stat.Mode.IsDir(),
|
||||
}
|
||||
|
||||
preArchive := content
|
||||
if srcInfo.RebaseName != "" {
|
||||
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
|
||||
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
|
||||
}
|
||||
|
||||
if err := archive.CopyTo(preArchive, srcInfo, out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dockerfile := `FROM docker/compose-bridge-transformer
|
||||
LABEL com.docker.compose.bridge=transformation
|
||||
COPY templates /templates
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(out, "Dockerfile"), []byte(dockerfile), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(dockerCli.Out(), "Transformer created in %q\n", out)
|
||||
return err
|
||||
}
|
||||
|
||||
func ListTransformers(ctx context.Context, dockerCli command.Cli) ([]image.Summary, error) {
|
||||
api := dockerCli.Client()
|
||||
return api.ImageList(ctx, image.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", fmt.Sprintf("%s=%s", TransformerLabel, "transformation")),
|
||||
),
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -22,19 +22,16 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/controller/pb"
|
||||
"github.com/docker/buildx/store/storeutil"
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
xprogress "github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/hints"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -71,10 +68,6 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
|
||||
}, s.stdinfo(), "Building")
|
||||
}
|
||||
|
||||
const bakeSuggest = "Compose can now delegate builds to bake for better performance.\n To do so, set COMPOSE_BAKE=true."
|
||||
|
||||
var suggest sync.Once
|
||||
|
||||
//nolint:gocyclo
|
||||
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]api.ImageSummary) (map[string]string, error) {
|
||||
imageIDs := map[string]string{}
|
||||
@@ -156,11 +149,6 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||
w *xprogress.Printer
|
||||
)
|
||||
if buildkitEnabled {
|
||||
if hints.Enabled() && progress.Mode != progress.ModeQuiet && progress.Mode != progress.ModeJSON {
|
||||
suggest.Do(func() {
|
||||
fmt.Fprintln(s.dockerCli.Out(), bakeSuggest) //nolint:errcheck
|
||||
})
|
||||
}
|
||||
builderName := options.Builder
|
||||
if builderName == "" {
|
||||
builderName = os.Getenv("BUILDX_BUILDER")
|
||||
@@ -184,7 +172,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),
|
||||
@@ -368,6 +356,7 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
|
||||
Variant: inspect.Variant,
|
||||
}
|
||||
if !platforms.NewMatcher(platform).Match(actual) {
|
||||
logrus.Debugf("local image %s doesn't match expected platform %s", service.Image, service.Platform)
|
||||
// there is a local image, but it's for the wrong platform, so
|
||||
// pretend it doesn't exist so that we can pull/build an image
|
||||
// for the correct platform instead
|
||||
@@ -493,8 +482,8 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
|
||||
DockerfilePath: dockerFilePath(service.Build.Context, service.Build.Dockerfile),
|
||||
NamedContexts: toBuildContexts(service, project),
|
||||
},
|
||||
CacheFrom: pb.CreateCaches(cacheFrom.ToPB()),
|
||||
CacheTo: pb.CreateCaches(cacheTo.ToPB()),
|
||||
CacheFrom: build.CreateCaches(cacheFrom),
|
||||
CacheTo: build.CreateCaches(cacheTo),
|
||||
NoCache: service.Build.NoCache,
|
||||
Pull: service.Build.Pull,
|
||||
BuildArgs: flatten(resolveAndMergeBuildArgs(s.dockerCli, project, service, options)),
|
||||
@@ -638,7 +627,11 @@ func parsePlatforms(service types.ServiceConfig) ([]specs.Platform, error) {
|
||||
func addBuildDependencies(services []string, project *types.Project) []string {
|
||||
servicesWithDependencies := utils.NewSet(services...)
|
||||
for _, service := range services {
|
||||
b := project.Services[service].Build
|
||||
s, ok := project.Services[service]
|
||||
if !ok {
|
||||
s = project.DisabledServices[service]
|
||||
}
|
||||
b := s.Build
|
||||
if b != nil {
|
||||
for _, target := range b.AdditionalContexts {
|
||||
if s, found := strings.CutPrefix(target, types.ServicePrefix); found {
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -32,7 +34,6 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/socket"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
@@ -43,20 +44,13 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
func buildWithBake(dockerCli command.Cli) (bool, error) {
|
||||
b, ok := os.LookupEnv("COMPOSE_BAKE")
|
||||
if !ok {
|
||||
if dockerCli.ConfigFile().Plugins["compose"]["build"] == "bake" {
|
||||
b, ok = "true", true
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
b = "true"
|
||||
}
|
||||
bake, err := strconv.ParseBool(b)
|
||||
if err != nil {
|
||||
@@ -72,6 +66,7 @@ func buildWithBake(dockerCli command.Cli) (bool, error) {
|
||||
}
|
||||
if !enabled {
|
||||
logrus.Warnf("Docker Compose is configured to build using Bake, but buildkit isn't enabled")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
_, err = manager.GetPlugin("buildx", dockerCli, &cobra.Command{})
|
||||
@@ -117,6 +112,7 @@ type bakeTarget struct {
|
||||
Ulimits []string `json:"ulimits,omitempty"`
|
||||
Call string `json:"call,omitempty"`
|
||||
Entitlements []string `json:"entitlements,omitempty"`
|
||||
ExtraHosts map[string]string `json:"extra-hosts,omitempty"`
|
||||
Outputs []string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
@@ -144,13 +140,25 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
Targets: map[string]bakeTarget{},
|
||||
}
|
||||
var (
|
||||
group bakeGroup
|
||||
privileged bool
|
||||
read []string
|
||||
expectedImages = make(map[string]string, len(serviceToBeBuild)) // service name -> expected image
|
||||
group bakeGroup
|
||||
privileged bool
|
||||
read []string
|
||||
targets = make(map[string]string, len(serviceToBeBuild)) // service name -> build target
|
||||
)
|
||||
|
||||
for serviceName, service := range serviceToBeBuild {
|
||||
// produce a unique ID for service used as bake target
|
||||
for serviceName := range project.Services {
|
||||
t := strings.ReplaceAll(serviceName, ".", "_")
|
||||
for {
|
||||
if _, ok := targets[serviceName]; !ok {
|
||||
targets[serviceName] = t
|
||||
break
|
||||
}
|
||||
t += "_"
|
||||
}
|
||||
}
|
||||
|
||||
for serviceName, service := range project.Services {
|
||||
if service.Build == nil {
|
||||
continue
|
||||
}
|
||||
@@ -164,9 +172,6 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
args[k] = *v
|
||||
}
|
||||
|
||||
image := api.GetImageNameOrDefault(service, project.Name)
|
||||
expectedImages[serviceName] = image
|
||||
|
||||
entitlements := build.Entitlements
|
||||
if slices.Contains(build.Entitlements, "security.insecure") {
|
||||
privileged = true
|
||||
@@ -196,17 +201,19 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Targets[serviceName] = bakeTarget{
|
||||
target := targets[serviceName]
|
||||
cfg.Targets[target] = bakeTarget{
|
||||
Context: build.Context,
|
||||
Contexts: additionalContexts(build.AdditionalContexts),
|
||||
Contexts: additionalContexts(build.AdditionalContexts, targets),
|
||||
Dockerfile: dockerFilePath(build.Context, build.Dockerfile),
|
||||
DockerfileInline: strings.ReplaceAll(build.DockerfileInline, "${", "$${"),
|
||||
Args: args,
|
||||
Labels: build.Labels,
|
||||
Tags: append(build.Tags, image),
|
||||
Tags: append(build.Tags, api.GetImageNameOrDefault(service, project.Name)),
|
||||
|
||||
CacheFrom: build.CacheFrom,
|
||||
// CacheTo: TODO
|
||||
CacheFrom: build.CacheFrom,
|
||||
CacheTo: build.CacheTo,
|
||||
NetworkMode: build.Network,
|
||||
Platforms: build.Platforms,
|
||||
Target: build.Target,
|
||||
Secrets: toBakeSecrets(project, build.Secrets),
|
||||
@@ -216,11 +223,19 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
ShmSize: build.ShmSize,
|
||||
Ulimits: toBakeUlimits(build.Ulimits),
|
||||
Entitlements: entitlements,
|
||||
ExtraHosts: toBakeExtraHosts(build.ExtraHosts),
|
||||
|
||||
Outputs: outputs,
|
||||
Call: call,
|
||||
}
|
||||
group.Targets = append(group.Targets, serviceName)
|
||||
}
|
||||
|
||||
// create a bake group with targets for services to build
|
||||
for serviceName, service := range serviceToBeBuild {
|
||||
if service.Build == nil {
|
||||
continue
|
||||
}
|
||||
group.Targets = append(group.Targets, targets[serviceName])
|
||||
}
|
||||
|
||||
cfg.Groups["default"] = group
|
||||
@@ -236,17 +251,25 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
}
|
||||
logrus.Debugf("bake build config:\n%s", string(b))
|
||||
|
||||
metadata, err := os.CreateTemp(os.TempDir(), "compose")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var metadataFile string
|
||||
for {
|
||||
// we don't use os.CreateTemp here as we need a temporary file name, but don't want it actually created
|
||||
// as bake relies on atomicwriter and this creates conflict during rename
|
||||
metadataFile = filepath.Join(os.TempDir(), fmt.Sprintf("compose-build-metadataFile-%d.json", rand.Int31()))
|
||||
if _, err = os.Stat(metadataFile); os.IsNotExist(err) {
|
||||
break
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(metadataFile)
|
||||
}()
|
||||
|
||||
buildx, err := manager.GetPlugin("buildx", s.dockerCli, &cobra.Command{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := []string{"bake", "--file", "-", "--progress", "rawjson", "--metadata-file", metadata.Name()}
|
||||
args := []string{"bake", "--file", "-", "--progress", "rawjson", "--metadata-file", metadataFile}
|
||||
mustAllow := buildx.Version != "" && versions.GreaterThanOrEqualTo(buildx.Version[1:], "0.17.0")
|
||||
if mustAllow {
|
||||
// FIXME we should prompt user about this, but this is a breaking change in UX
|
||||
@@ -268,24 +291,12 @@ 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)
|
||||
err = s.prepareShellOut(ctx, project, cmd)
|
||||
if err != nil {
|
||||
defer server.Close() //nolint:errcheck
|
||||
cmd.Cancel = server.Close
|
||||
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
|
||||
|
||||
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
|
||||
carrier := propagation.MapCarrier{}
|
||||
otel.GetTextMapPropagator().Inject(ctx, &carrier)
|
||||
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
|
||||
|
||||
cmd.Stdout = s.stdout()
|
||||
cmd.Stdin = bytes.NewBuffer(b)
|
||||
pipe, err := cmd.StderrPipe()
|
||||
@@ -294,16 +305,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)
|
||||
@@ -327,7 +344,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
return nil, fmt.Errorf("failed to execute bake: %w", err)
|
||||
}
|
||||
|
||||
b, err = os.ReadFile(metadata.Name())
|
||||
b, err = os.ReadFile(metadataFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -340,10 +357,11 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
|
||||
cw := progress.ContextWriter(ctx)
|
||||
results := map[string]string{}
|
||||
for service, name := range expectedImages {
|
||||
built, ok := md[service] // bake target == service name
|
||||
for name := range serviceToBeBuild {
|
||||
target := targets[name]
|
||||
built, ok := md[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("build result not found in Bake metadata for service %s", service)
|
||||
return nil, fmt.Errorf("build result not found in Bake metadata for service %s", name)
|
||||
}
|
||||
results[name] = built.Digest
|
||||
cw.Event(progress.BuiltEvent(name))
|
||||
@@ -351,11 +369,19 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func additionalContexts(contexts types.Mapping) map[string]string {
|
||||
func toBakeExtraHosts(hosts types.HostsList) map[string]string {
|
||||
m := make(map[string]string)
|
||||
for k, v := range hosts {
|
||||
m[k] = strings.Join(v, ",")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func additionalContexts(contexts types.Mapping, targets map[string]string) map[string]string {
|
||||
ac := map[string]string{}
|
||||
for k, v := range contexts {
|
||||
if target, found := strings.CutPrefix(v, types.ServicePrefix); found {
|
||||
v = "target:" + target
|
||||
v = "target:" + targets[target]
|
||||
}
|
||||
ac[k] = v
|
||||
}
|
||||
@@ -400,28 +426,20 @@ 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)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func replace(environ []string, variable, value string) []string {
|
||||
filtered := filter(environ, variable)
|
||||
return append(filtered, fmt.Sprintf("%s=%s", variable, value))
|
||||
}
|
||||
|
||||
func dockerFilePath(ctxName string, dockerfile string) string {
|
||||
if dockerfile == "" {
|
||||
return ""
|
||||
}
|
||||
if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
|
||||
if urlutil.IsGitURL(ctxName) {
|
||||
return dockerfile
|
||||
}
|
||||
return filepath.Join(ctxName, dockerfile)
|
||||
if !filepath.IsAbs(dockerfile) {
|
||||
dockerfile = filepath.Join(ctxName, dockerfile)
|
||||
}
|
||||
dir := filepath.Dir(dockerfile)
|
||||
symlinks, err := filepath.EvalSymlinks(dir)
|
||||
if err == nil {
|
||||
return filepath.Join(symlinks, filepath.Base(dockerfile))
|
||||
}
|
||||
return dockerfile
|
||||
}
|
||||
|
||||
@@ -27,23 +27,19 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image/build"
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
buildtypes "github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/builder/remotecontext/urlutil"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/moby/go-archive"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -179,7 +175,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj
|
||||
|
||||
imageID := ""
|
||||
aux := func(msg jsonmessage.JSONMessage) {
|
||||
var result dockertypes.BuildResult
|
||||
var result buildtypes.Result
|
||||
if err := json.Unmarshal(*msg.Aux, &result); err != nil {
|
||||
logrus.Errorf("Failed to parse aux message: %s", err)
|
||||
} else {
|
||||
@@ -219,10 +215,10 @@ func isLocalDir(c string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func imageBuildOptions(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, options api.BuildOptions) dockertypes.ImageBuildOptions {
|
||||
func imageBuildOptions(dockerCli command.Cli, project *types.Project, service types.ServiceConfig, options api.BuildOptions) buildtypes.ImageBuildOptions {
|
||||
config := service.Build
|
||||
return dockertypes.ImageBuildOptions{
|
||||
Version: dockertypes.BuilderV1,
|
||||
return buildtypes.ImageBuildOptions{
|
||||
Version: buildtypes.BuilderV1,
|
||||
Tags: config.Tags,
|
||||
NoCache: config.NoCache,
|
||||
Remove: true,
|
||||
|
||||
@@ -69,7 +69,7 @@ func (s *composeService) commit(ctx context.Context, projectName string, options
|
||||
Reference: options.Reference,
|
||||
Comment: options.Comment,
|
||||
Author: options.Author,
|
||||
Changes: options.Changes.GetAll(),
|
||||
Changes: options.Changes.GetSlice(),
|
||||
Pause: options.Pause,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -19,12 +19,12 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
@@ -124,7 +124,7 @@ func matches(c container.Summary, predicates ...containerPredicate) bool {
|
||||
func isService(services ...string) containerPredicate {
|
||||
return func(c container.Summary) bool {
|
||||
service := c.Labels[api.ServiceLabel]
|
||||
return utils.StringContains(services, service)
|
||||
return slices.Contains(services, service)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ func isOrphaned(project *types.Project) containerPredicate {
|
||||
}
|
||||
// Service that is not defined in the compose model
|
||||
service := c.Labels[api.ServiceLabel]
|
||||
return !utils.StringContains(services, service)
|
||||
return !slices.Contains(services, service)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options
|
||||
|
||||
return tracing.SpanWrapFunc("service/apply", tracing.ServiceOptions(service), func(ctx context.Context) error {
|
||||
strategy := options.RecreateDependencies
|
||||
if utils.StringContains(options.Services, name) {
|
||||
if slices.Contains(options.Services, name) {
|
||||
strategy = options.Recreate
|
||||
}
|
||||
return c.ensureService(ctx, project, service, strategy, options.Inherit, options.Timeout)
|
||||
@@ -191,7 +191,6 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
|
||||
case ContainerCreated:
|
||||
case ContainerRestarting:
|
||||
case ContainerExited:
|
||||
w.Event(progress.CreatedEvent(name))
|
||||
default:
|
||||
container := container
|
||||
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/start", tracing.ContainerOptions(container), func(ctx context.Context) error {
|
||||
@@ -564,6 +563,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
|
||||
}
|
||||
@@ -757,14 +759,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()
|
||||
@@ -898,6 +893,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{})
|
||||
|
||||
@@ -24,27 +24,27 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/paths"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/prompt"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/docker/api/types/blkiodev"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"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/docker/errdefs"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/sirupsen/logrus"
|
||||
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/prompt"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
@@ -82,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)
|
||||
@@ -113,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)
|
||||
}
|
||||
|
||||
@@ -168,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 (
|
||||
@@ -273,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),
|
||||
@@ -1253,7 +1262,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
|
||||
}
|
||||
|
||||
id, err := s.resolveOrCreateNetwork(ctx, project, name, n)
|
||||
if errdefs.IsConflict(err) {
|
||||
if cerrdefs.IsConflict(err) {
|
||||
// Maybe another execution of `docker compose up|run` created same network
|
||||
// let's retry once
|
||||
return s.resolveOrCreateNetwork(ctx, project, name, n)
|
||||
@@ -1262,6 +1271,9 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
|
||||
}
|
||||
|
||||
func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (string, error) { //nolint:gocyclo
|
||||
// This is containers that could be left after a diverged network was removed
|
||||
var dangledContainers Containers
|
||||
|
||||
// First, try to find a unique network matching by name or ID
|
||||
inspect, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
|
||||
if err == nil {
|
||||
@@ -1295,7 +1307,7 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
|
||||
return inspect.ID, nil
|
||||
}
|
||||
|
||||
err = s.removeDivergedNetwork(ctx, project, name, n)
|
||||
dangledContainers, err = s.removeDivergedNetwork(ctx, project, name, n)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -1312,8 +1324,8 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
|
||||
}
|
||||
|
||||
// NetworkList Matches all or part of a network name, so we have to filter for a strict match
|
||||
networks = utils.Filter(networks, func(net network.Summary) bool {
|
||||
return net.Name == n.Name
|
||||
networks = slices.DeleteFunc(networks, func(net network.Summary) bool {
|
||||
return net.Name != n.Name
|
||||
})
|
||||
|
||||
for _, net := range networks {
|
||||
@@ -1392,10 +1404,16 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
|
||||
return "", fmt.Errorf("failed to create network %s: %w", n.Name, err)
|
||||
}
|
||||
w.Event(progress.CreatedEvent(networkEventName))
|
||||
|
||||
err = s.connectNetwork(ctx, n.Name, dangledContainers, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
func (s *composeService) removeDivergedNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) error {
|
||||
func (s *composeService) removeDivergedNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (Containers, error) {
|
||||
// Remove services attached to this network to force recreation
|
||||
var services []string
|
||||
for _, service := range project.Services.Filter(func(config types.ServiceConfig) bool {
|
||||
@@ -1412,13 +1430,54 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
|
||||
Project: project,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, true, services...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.disconnectNetwork(ctx, n.Name, containers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.apiClient().NetworkRemove(ctx, n.Name)
|
||||
eventName := fmt.Sprintf("Network %s", n.Name)
|
||||
progress.ContextWriter(ctx).Event(progress.RemovedEvent(eventName))
|
||||
return err
|
||||
return containers, err
|
||||
}
|
||||
|
||||
func (s *composeService) disconnectNetwork(
|
||||
ctx context.Context,
|
||||
network string,
|
||||
containers Containers,
|
||||
) error {
|
||||
for _, c := range containers {
|
||||
err := s.apiClient().NetworkDisconnect(ctx, network, c.ID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) connectNetwork(
|
||||
ctx context.Context,
|
||||
network string,
|
||||
containers Containers,
|
||||
config *network.EndpointSettings,
|
||||
) error {
|
||||
for _, c := range containers {
|
||||
err := s.apiClient().NetworkConnect(ctx, network, c.ID, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.NetworkConfig) (string, error) {
|
||||
@@ -1436,18 +1495,19 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
|
||||
if len(networks) == 0 {
|
||||
// in this instance, n.Name is really an ID
|
||||
sn, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
|
||||
if err != nil && !errdefs.IsNotFound(err) {
|
||||
if err == nil {
|
||||
networks = append(networks, sn)
|
||||
} else if !cerrdefs.IsNotFound(err) {
|
||||
return "", err
|
||||
}
|
||||
networks = append(networks, sn)
|
||||
|
||||
}
|
||||
|
||||
// NetworkList API doesn't return the exact name match, so we can retrieve more than one network with a request
|
||||
networks = utils.Filter(networks, func(net network.Inspect) bool {
|
||||
// later in this function, the name is changed the to ID.
|
||||
networks = slices.DeleteFunc(networks, func(net network.Inspect) bool {
|
||||
// this function is called during the rebuild stage of `compose watch`.
|
||||
// we still require just one network back, but we need to run the search on the ID
|
||||
return net.Name == n.Name || net.ID == n.Name
|
||||
return net.Name != n.Name && net.ID != n.Name
|
||||
})
|
||||
|
||||
switch len(networks) {
|
||||
@@ -1474,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 !errdefs.IsNotFound(err) {
|
||||
if !cerrdefs.IsNotFound(err) {
|
||||
return "", err
|
||||
}
|
||||
if volume.External {
|
||||
|
||||
@@ -19,14 +19,13 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// ServiceStatus indicates the status of a service
|
||||
@@ -119,7 +118,7 @@ func WithRootNodesAndDown(nodes []string) func(*graphTraversal) {
|
||||
|
||||
t.ignored = map[string]struct{}{}
|
||||
for k := range graph.Vertices {
|
||||
if !utils.Contains(want, k) {
|
||||
if !slices.Contains(want, k) {
|
||||
t.ignored[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -434,7 +433,7 @@ func (g *Graph) HasCycles() (bool, error) {
|
||||
path := []string{
|
||||
vertex.Key,
|
||||
}
|
||||
if !utils.StringContains(discovered, vertex.Key) && !utils.StringContains(finished, vertex.Key) {
|
||||
if !slices.Contains(discovered, vertex.Key) && !slices.Contains(finished, vertex.Key) {
|
||||
var err error
|
||||
discovered, finished, err = g.visit(vertex.Key, path, discovered, finished)
|
||||
if err != nil {
|
||||
@@ -451,11 +450,11 @@ func (g *Graph) visit(key string, path []string, discovered []string, finished [
|
||||
|
||||
for _, v := range g.Vertices[key].Children {
|
||||
path := append(path, v.Key)
|
||||
if utils.StringContains(discovered, v.Key) {
|
||||
if slices.Contains(discovered, v.Key) {
|
||||
return nil, nil, fmt.Errorf("cycle found: %s", strings.Join(path, " -> "))
|
||||
}
|
||||
|
||||
if !utils.StringContains(finished, v.Key) {
|
||||
if !slices.Contains(finished, v.Key) {
|
||||
if _, _, err := g.visit(v.Key, path, discovered, finished); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
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"
|
||||
@@ -30,7 +31,6 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
imageapi "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -219,7 +219,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
continue
|
||||
}
|
||||
nw, err := s.apiClient().NetworkInspect(ctx, net.ID, network.InspectOptions{})
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
}
|
||||
|
||||
if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
w.Event(progress.ErrorEvent(eventName))
|
||||
@@ -261,11 +261,11 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre
|
||||
w.Event(progress.NewEvent(id, progress.Done, "Removed"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsConflict(err) {
|
||||
if cerrdefs.IsConflict(err) {
|
||||
w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
@@ -276,7 +276,7 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
|
||||
resource := fmt.Sprintf("Volume %s", id)
|
||||
|
||||
_, err := s.apiClient().VolumeInspect(ctx, id)
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
// Already gone
|
||||
return nil
|
||||
}
|
||||
@@ -287,11 +287,11 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
|
||||
w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsConflict(err) {
|
||||
if cerrdefs.IsConflict(err) {
|
||||
w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
@@ -306,6 +306,10 @@ func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, s
|
||||
for _, hook := range service.PreStop {
|
||||
err := s.runHook(ctx, ctr, *service, hook, nil)
|
||||
if err != nil {
|
||||
// Ignore errors indicating that some containers were already stopped or removed.
|
||||
if cerrdefs.IsNotFound(err) || cerrdefs.IsConflict(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -345,7 +349,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
|
||||
w := progress.ContextWriter(ctx)
|
||||
eventName := getContainerProgressName(ctr)
|
||||
err := s.stopContainer(ctx, w, service, ctr, timeout)
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
w.Event(progress.RemovedEvent(eventName))
|
||||
return nil
|
||||
}
|
||||
@@ -357,7 +361,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
|
||||
Force: true,
|
||||
RemoveVolumes: volumes,
|
||||
})
|
||||
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
|
||||
if err != nil && !cerrdefs.IsNotFound(err) && !cerrdefs.IsConflict(err) {
|
||||
w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,7 +26,6 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func (s *composeService) Events(ctx context.Context, projectName string, options api.EventsOptions) error {
|
||||
@@ -47,7 +47,7 @@ func (s *composeService) Events(ctx context.Context, projectName string, options
|
||||
continue
|
||||
}
|
||||
service := event.Actor.Attributes[api.ServiceLabel]
|
||||
if len(options.Services) > 0 && !utils.StringContains(options.Services, service) {
|
||||
if len(options.Services) > 0 && !slices.Contains(options.Services, service) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -19,17 +19,16 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
func (s *composeService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
|
||||
@@ -54,8 +53,11 @@ func (s *composeService) Generate(ctx context.Context, options api.GenerateOptio
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ctr := range containersByIds {
|
||||
if !utils.Contains(containers, ctr) {
|
||||
if !slices.ContainsFunc(containers, func(summary container.Summary) bool {
|
||||
return summary.ID == ctr.ID
|
||||
}) {
|
||||
containers = append(containers, ctr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 +23,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -204,7 +204,7 @@ func (p *ImagePruner) filterImagesByExistence(ctx context.Context, imageNames []
|
||||
for _, img := range imageNames {
|
||||
eg.Go(func() error {
|
||||
_, err := p.client.ImageInspect(ctx, img)
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
// err on the side of caution: only skip if we successfully
|
||||
// queried the API and got back a definitive "not exists"
|
||||
return nil
|
||||
|
||||
@@ -19,20 +19,23 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
|
||||
func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) (map[string]api.ImageSummary, error) {
|
||||
projectName = strings.ToLower(projectName)
|
||||
allContainers, err := s.apiClient().ContainerList(ctx, container.ListOptions{
|
||||
All: true,
|
||||
@@ -45,7 +48,7 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
|
||||
if len(options.Services) > 0 {
|
||||
// filter service containers
|
||||
for _, c := range allContainers {
|
||||
if utils.StringContains(options.Services, c.Labels[api.ServiceLabel]) {
|
||||
if slices.Contains(options.Services, c.Labels[api.ServiceLabel]) {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
}
|
||||
@@ -53,27 +56,61 @@ func (s *composeService) Images(ctx context.Context, projectName string, options
|
||||
containers = allContainers
|
||||
}
|
||||
|
||||
images := []string{}
|
||||
for _, c := range containers {
|
||||
if !utils.StringContains(images, c.Image) {
|
||||
images = append(images, c.Image)
|
||||
}
|
||||
}
|
||||
imageSummaries, err := s.getImageSummaries(ctx, images)
|
||||
version, err := s.RuntimeVersion(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary := make([]api.ImageSummary, len(containers))
|
||||
for i, c := range containers {
|
||||
img, ok := imageSummaries[c.Image]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to retrieve image for container %s", getCanonicalContainerName(c))
|
||||
}
|
||||
withPlatform := versions.GreaterThanOrEqualTo(version, "1.49")
|
||||
|
||||
summary[i] = img
|
||||
summary[i].ContainerName = getCanonicalContainerName(c)
|
||||
summary := map[string]api.ImageSummary{}
|
||||
var mux sync.Mutex
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, c := range containers {
|
||||
eg.Go(func() error {
|
||||
image, err := s.apiClient().ImageInspect(ctx, c.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := image.ID // platform-specific image ID can't be combined with image tag, see https://github.com/moby/moby/issues/49995
|
||||
|
||||
if withPlatform && c.ImageManifestDescriptor != nil && c.ImageManifestDescriptor.Platform != nil {
|
||||
image, err = s.apiClient().ImageInspect(ctx, c.Image, client.ImageInspectWithPlatform(c.ImageManifestDescriptor.Platform))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var repository, tag string
|
||||
ref, err := reference.ParseDockerRef(c.Image)
|
||||
if err == nil {
|
||||
// ParseDockerRef will reject a local image ID
|
||||
repository = reference.FamiliarName(ref)
|
||||
if tagged, ok := ref.(reference.Tagged); ok {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
}
|
||||
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
summary[getCanonicalContainerName(c)] = api.ImageSummary{
|
||||
ID: id,
|
||||
Repository: repository,
|
||||
Tag: tag,
|
||||
Platform: platforms.Platform{
|
||||
Architecture: image.Architecture,
|
||||
OS: image.Os,
|
||||
OSVersion: image.OsVersion,
|
||||
Variant: image.Variant,
|
||||
},
|
||||
Size: image.Size,
|
||||
LastTagTime: image.Metadata.LastTagTime,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return summary, nil
|
||||
|
||||
err = eg.Wait()
|
||||
return summary, err
|
||||
}
|
||||
|
||||
func (s *composeService) getImageSummaries(ctx context.Context, repoTags []string) (map[string]api.ImageSummary, error) {
|
||||
@@ -84,7 +121,7 @@ func (s *composeService) getImageSummaries(ctx context.Context, repoTags []strin
|
||||
eg.Go(func() error {
|
||||
inspect, err := s.apiClient().ImageInspect(ctx, repoTag)
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unable to get image '%s': %w", repoTag, err)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
@@ -42,9 +43,10 @@ func TestImages(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
args := filters.NewArgs(projectFilter(strings.ToLower(testProject)))
|
||||
listOpts := container.ListOptions{All: true, Filters: args}
|
||||
api.EXPECT().ServerVersion(gomock.Any()).Return(types.Version{APIVersion: "1.96"}, nil).AnyTimes()
|
||||
image1 := imageInspect("image1", "foo:1", 12345)
|
||||
image2 := imageInspect("image2", "bar:2", 67890)
|
||||
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(image1, nil)
|
||||
api.EXPECT().ImageInspect(anyCancellableContext(), "foo:1").Return(image1, nil).MaxTimes(2)
|
||||
api.EXPECT().ImageInspect(anyCancellableContext(), "bar:2").Return(image2, nil)
|
||||
c1 := containerDetail("service1", "123", "running", "foo:1")
|
||||
c2 := containerDetail("service1", "456", "running", "bar:2")
|
||||
@@ -54,27 +56,24 @@ func TestImages(t *testing.T) {
|
||||
|
||||
images, err := tested.Images(ctx, strings.ToLower(testProject), compose.ImagesOptions{})
|
||||
|
||||
expected := []compose.ImageSummary{
|
||||
{
|
||||
ID: "image1",
|
||||
ContainerName: "123",
|
||||
Repository: "foo",
|
||||
Tag: "1",
|
||||
Size: 12345,
|
||||
expected := map[string]compose.ImageSummary{
|
||||
"123": {
|
||||
ID: "image1",
|
||||
Repository: "foo",
|
||||
Tag: "1",
|
||||
Size: 12345,
|
||||
},
|
||||
{
|
||||
ID: "image2",
|
||||
ContainerName: "456",
|
||||
Repository: "bar",
|
||||
Tag: "2",
|
||||
Size: 67890,
|
||||
"456": {
|
||||
ID: "image2",
|
||||
Repository: "bar",
|
||||
Tag: "2",
|
||||
Size: 67890,
|
||||
},
|
||||
{
|
||||
ID: "image1",
|
||||
ContainerName: "789",
|
||||
Repository: "foo",
|
||||
Tag: "1",
|
||||
Size: 12345,
|
||||
"789": {
|
||||
ID: "image1",
|
||||
Repository: "foo",
|
||||
Tag: "1",
|
||||
Size: 12345,
|
||||
},
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -18,12 +18,11 @@ 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"
|
||||
@@ -64,8 +63,7 @@ func (s *composeService) Logs(
|
||||
for _, ctr := range containers {
|
||||
eg.Go(func() error {
|
||||
err := s.logContainers(ctx, consumer, ctr, options)
|
||||
var notImplErr errdefs.ErrNotImplemented
|
||||
if errors.As(err, ¬ImplErr) {
|
||||
if errdefs.IsNotImplemented(err) {
|
||||
logrus.Warnf("Can't retrieve logs for %q: %s", getCanonicalContainerName(ctr), err.Error())
|
||||
return nil
|
||||
}
|
||||
@@ -106,8 +104,7 @@ func (s *composeService) Logs(
|
||||
Tail: options.Tail,
|
||||
Timestamps: options.Timestamps,
|
||||
})
|
||||
var notImplErr errdefs.ErrNotImplemented
|
||||
if errors.As(err, ¬ImplErr) {
|
||||
if errdefs.IsNotImplemented(err) {
|
||||
// ignore
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -74,7 +74,7 @@ func combinedConfigFiles(containers []container.Summary) (string, error) {
|
||||
}
|
||||
|
||||
for _, f := range strings.Split(files, ",") {
|
||||
if !utils.StringContains(configFiles, f) {
|
||||
if !slices.Contains(configFiles, f) {
|
||||
configFiles = append(configFiles, f)
|
||||
}
|
||||
}
|
||||
|
||||
230
pkg/compose/model.go
Normal file
230
pkg/compose/model.go
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
dockerModel, err := manager.GetPlugin("model", s.dockerCli, &cobra.Command{})
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
return fmt.Errorf("'models' support requires Docker Model plugin")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, dockerModel.Path, "ls", "--json")
|
||||
err = s.prepareShellOut(ctx, project, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return 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 fmt.Errorf("error unmarshalling available models: %w", err)
|
||||
}
|
||||
var availableModels []string
|
||||
for _, model := range models {
|
||||
availableModels = append(availableModels, model.Tags...)
|
||||
}
|
||||
|
||||
eg, gctx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
return s.setModelVariables(gctx, dockerModel, project)
|
||||
})
|
||||
|
||||
for name, config := range project.Models {
|
||||
if config.Name == "" {
|
||||
config.Name = name
|
||||
}
|
||||
eg.Go(func() error {
|
||||
w := progress.ContextWriter(gctx)
|
||||
if !slices.Contains(availableModels, config.Model) {
|
||||
err = s.pullModel(gctx, dockerModel, project, config, quietPull, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.configureModel(gctx, dockerModel, project, config, w)
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) pullModel(ctx context.Context, dockerModel *manager.Plugin, project *types.Project, 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, dockerModel.Path, "pull", model.Model)
|
||||
err := s.prepareShellOut(ctx, project, 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 (s *composeService) configureModel(ctx context.Context, dockerModel *manager.Plugin, project *types.Project, 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, dockerModel.Path, args...)
|
||||
err := s.prepareShellOut(ctx, project, cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (s *composeService) setModelVariables(ctx context.Context, dockerModel *manager.Plugin, project *types.Project) error {
|
||||
cmd := exec.CommandContext(ctx, dockerModel.Path, "status", "--json")
|
||||
err := s.prepareShellOut(ctx, project, 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
|
||||
}
|
||||
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"`
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -24,16 +25,16 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/socket"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
)
|
||||
|
||||
type JsonMessage struct {
|
||||
@@ -42,12 +43,15 @@ type JsonMessage struct {
|
||||
}
|
||||
|
||||
const (
|
||||
ErrorType = "error"
|
||||
InfoType = "info"
|
||||
SetEnvType = "setenv"
|
||||
DebugType = "debug"
|
||||
ErrorType = "error"
|
||||
InfoType = "info"
|
||||
SetEnvType = "setenv"
|
||||
DebugType = "debug"
|
||||
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
|
||||
|
||||
@@ -56,13 +60,18 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := s.setupPluginCommand(ctx, project, service, plugin, command)
|
||||
cmd, err := s.setupPluginCommand(ctx, project, service, plugin, command)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
variables, err := s.executePlugin(ctx, cmd, command, service)
|
||||
if err != nil {
|
||||
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) + "_"
|
||||
@@ -160,41 +169,114 @@ func (s *composeService) getPluginBinaryPath(provider string) (path string, err
|
||||
return path, err
|
||||
}
|
||||
|
||||
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) *exec.Cmd {
|
||||
func (s *composeService) setupPluginCommand(ctx context.Context, project *types.Project, service types.ServiceConfig, path, command string) (*exec.Cmd, error) {
|
||||
cmdOptionsMetadata := s.getPluginMetadata(path, service.Provider.Type, project)
|
||||
var currentCommandMetadata CommandMetadata
|
||||
switch command {
|
||||
case "up":
|
||||
currentCommandMetadata = cmdOptionsMetadata.Up
|
||||
case "down":
|
||||
currentCommandMetadata = cmdOptionsMetadata.Down
|
||||
}
|
||||
|
||||
provider := *service.Provider
|
||||
commandMetadataIsEmpty := cmdOptionsMetadata.IsEmpty()
|
||||
if err := currentCommandMetadata.CheckRequiredParameters(provider); !commandMetadataIsEmpty && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := []string{"compose", "--project-name", project.Name, command}
|
||||
for k, v := range provider.Options {
|
||||
for _, value := range v {
|
||||
args = append(args, fmt.Sprintf("--%s=%s", k, value))
|
||||
if _, ok := currentCommandMetadata.GetParameter(k); commandMetadataIsEmpty || ok {
|
||||
args = append(args, fmt.Sprintf("--%s=%s", k, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
args = append(args, service.Name)
|
||||
|
||||
cmd := exec.CommandContext(ctx, path, args...)
|
||||
// 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, 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.Cancel = server.Close
|
||||
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
|
||||
|
||||
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
|
||||
carrier := propagation.MapCarrier{}
|
||||
otel.GetTextMapPropagator().Inject(ctx, &carrier)
|
||||
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
|
||||
return cmd
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (s *composeService) getPluginMetadata(path, command string, project *types.Project) ProviderMetadata {
|
||||
cmd := exec.Command(path, "compose", "metadata")
|
||||
err := s.prepareShellOut(context.Background(), project, cmd)
|
||||
if err != nil {
|
||||
logrus.Debugf("failed to prepare plugin metadata command: %v", err)
|
||||
return ProviderMetadata{}
|
||||
}
|
||||
stdout := &bytes.Buffer{}
|
||||
cmd.Stdout = stdout
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
logrus.Debugf("failed to start plugin metadata command: %v", err)
|
||||
return ProviderMetadata{}
|
||||
}
|
||||
|
||||
var metadata ProviderMetadata
|
||||
if err := json.Unmarshal(stdout.Bytes(), &metadata); err != nil {
|
||||
output, _ := io.ReadAll(stdout)
|
||||
logrus.Debugf("failed to decode plugin metadata: %v - %s", err, output)
|
||||
return ProviderMetadata{}
|
||||
}
|
||||
// Save metadata into docker home directory to be used by Docker LSP tool
|
||||
// Just log the error as it's not a critical error for the main flow
|
||||
metadataDir := filepath.Join(config.Dir(), providerMetadataDirectory)
|
||||
if err := os.MkdirAll(metadataDir, 0o700); err == nil {
|
||||
metadataFilePath := filepath.Join(metadataDir, command+".json")
|
||||
if err := os.WriteFile(metadataFilePath, stdout.Bytes(), 0o600); err != nil {
|
||||
logrus.Debugf("failed to save plugin metadata: %v", err)
|
||||
}
|
||||
} else {
|
||||
logrus.Debugf("failed to create plugin metadata directory: %v", err)
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
type ProviderMetadata struct {
|
||||
Description string `json:"description"`
|
||||
Up CommandMetadata `json:"up"`
|
||||
Down CommandMetadata `json:"down"`
|
||||
}
|
||||
|
||||
func (p ProviderMetadata) IsEmpty() bool {
|
||||
return p.Description == "" && p.Up.Parameters == nil && p.Down.Parameters == nil
|
||||
}
|
||||
|
||||
type CommandMetadata struct {
|
||||
Parameters []ParameterMetadata `json:"parameters"`
|
||||
}
|
||||
|
||||
type ParameterMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
Type string `json:"type"`
|
||||
Default string `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
func (c CommandMetadata) GetParameter(paramName string) (ParameterMetadata, bool) {
|
||||
for _, p := range c.Parameters {
|
||||
if p.Name == paramName {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return ParameterMetadata{}, false
|
||||
}
|
||||
|
||||
func (c CommandMetadata) CheckRequiredParameters(provider types.ServiceProviderConfig) error {
|
||||
for _, p := range c.Parameters {
|
||||
if p.Required {
|
||||
if _, ok := provider.Options[p.Name]; !ok {
|
||||
return fmt.Errorf("required parameter %q is missing from provider %q definition", p.Name, provider.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -236,8 +236,20 @@ 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
|
||||
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
|
||||
}
|
||||
}
|
||||
if options.AssumeYes {
|
||||
return true, nil
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,12 +22,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli"
|
||||
cmd "github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
@@ -58,6 +59,19 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
||||
}
|
||||
|
||||
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
|
||||
// Temporary implementation of use_api_socket until we get actual support inside docker engine
|
||||
project, err := s.useAPISocket(project)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = progress.Run(ctx, func(ctx context.Context) error {
|
||||
return s.startDependencies(ctx, project, opts)
|
||||
}, s.stdinfo())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
service, err := project.GetService(opts.Service)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -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) {
|
||||
@@ -130,11 +156,11 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
|
||||
|
||||
if len(opts.CapAdd) > 0 {
|
||||
service.CapAdd = append(service.CapAdd, opts.CapAdd...)
|
||||
service.CapDrop = utils.Remove(service.CapDrop, opts.CapAdd...)
|
||||
service.CapDrop = slices.DeleteFunc(service.CapDrop, func(e string) bool { return slices.Contains(opts.CapAdd, e) })
|
||||
}
|
||||
if len(opts.CapDrop) > 0 {
|
||||
service.CapDrop = append(service.CapDrop, opts.CapDrop...)
|
||||
service.CapAdd = utils.Remove(service.CapAdd, opts.CapDrop...)
|
||||
service.CapAdd = slices.DeleteFunc(service.CapAdd, func(e string) bool { return slices.Contains(opts.CapDrop, e) })
|
||||
}
|
||||
if opts.WorkingDir != "" {
|
||||
service.WorkingDir = opts.WorkingDir
|
||||
@@ -160,3 +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
|
||||
}
|
||||
|
||||
63
pkg/compose/shellout.go
Normal file
63
pkg/compose/shellout.go
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
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/manager"
|
||||
"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, project *types.Project, cmd *exec.Cmd) error {
|
||||
// exec command with same environment Compose is running
|
||||
env := types.NewMapping(project.Environment.Values())
|
||||
|
||||
// remove DOCKER_CLI_PLUGIN... variable so a docker-cli plugin will detect it run standalone
|
||||
delete(env, manager.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
|
||||
|
||||
metadata, err := s.dockerCli.ContextStore().GetMetadata(s.dockerCli.CurrentContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpoint, err := docker.EndpointFromContext(metadata)
|
||||
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,15 +20,15 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/errdefs"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@@ -199,7 +199,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
ofInterest := func(c containerType.Summary) bool {
|
||||
if len(services) > 0 {
|
||||
// we only watch some services
|
||||
return utils.Contains(services, c.Labels[api.ServiceLabel])
|
||||
return slices.Contains(services, c.Labels[api.ServiceLabel])
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
isRequired := func(c containerType.Summary) bool {
|
||||
if len(services) > 0 && len(required) > 0 {
|
||||
// we only watch some services
|
||||
return utils.Contains(required, c.Labels[api.ServiceLabel])
|
||||
return slices.Contains(required, c.Labels[api.ServiceLabel])
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -237,7 +237,7 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
}()
|
||||
inspected, err := s.apiClient().ContainerInspect(ctx, event.Container)
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
// it's possible to get "destroy" or "kill" events but not
|
||||
// be able to inspect in time before they're gone from the
|
||||
// API, so just remove the watch without erroring
|
||||
@@ -263,8 +263,8 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
}
|
||||
if _, ok := watched[container.ID]; ok {
|
||||
eType := api.ContainerEventStopped
|
||||
if utils.Contains(replaced, container.ID) {
|
||||
utils.Remove(replaced, container.ID)
|
||||
if slices.Contains(replaced, container.ID) {
|
||||
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
|
||||
eType = api.ContainerEventRecreated
|
||||
}
|
||||
listener(api.ContainerEvent{
|
||||
@@ -290,8 +290,8 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
}
|
||||
|
||||
eType := api.ContainerEventExit
|
||||
if utils.Contains(replaced, container.ID) {
|
||||
utils.Remove(replaced, container.ID)
|
||||
if slices.Contains(replaced, container.ID) {
|
||||
replaced = slices.DeleteFunc(replaced, func(e string) bool { return e == container.ID })
|
||||
eType = api.ContainerEventRecreated
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
||||
@@ -51,7 +51,7 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a
|
||||
|
||||
w := progress.ContextWriter(ctx)
|
||||
return InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
|
||||
if !utils.StringContains(options.Services, service) {
|
||||
if !slices.Contains(options.Services, service) {
|
||||
return nil
|
||||
}
|
||||
serv := project.Services[service]
|
||||
|
||||
@@ -25,12 +25,12 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/eiannone/keyboard"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -70,9 +70,12 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer signal.Stop(signalChan)
|
||||
var isTerminated atomic.Bool
|
||||
printer := newLogPrinter(options.Start.Attach)
|
||||
|
||||
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 {
|
||||
@@ -80,20 +83,26 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
options.Start.NavigationMenu = false
|
||||
} else {
|
||||
defer keyboard.Close() //nolint:errcheck
|
||||
isWatchConfigured := s.shouldWatch(project)
|
||||
isDockerDesktopActive := s.isDesktopIntegrationActive()
|
||||
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured)
|
||||
formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, signalChan, s.watch)
|
||||
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive)
|
||||
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)
|
||||
|
||||
doneCh := make(chan bool)
|
||||
eg.Go(func() error {
|
||||
if options.Start.NavigationMenu && options.Start.Watch {
|
||||
// Run watch by navigation menu, so we can interactively enable/disable
|
||||
formatter.KeyboardManager.StartWatch(ctx, doneCh, project, options)
|
||||
}
|
||||
|
||||
first := true
|
||||
gracefulTeardown := func() {
|
||||
printer.Cancel()
|
||||
@@ -112,6 +121,9 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
for {
|
||||
select {
|
||||
case <-doneCh:
|
||||
if watcher != nil {
|
||||
return watcher.Stop()
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
if first {
|
||||
@@ -119,6 +131,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
}
|
||||
case <-signalChan:
|
||||
if first {
|
||||
keyboard.Close() //nolint:errcheck
|
||||
gracefulTeardown()
|
||||
break
|
||||
}
|
||||
@@ -129,7 +142,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
All: true,
|
||||
})
|
||||
// Ignore errors indicating that some of the containers were already stopped or removed.
|
||||
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) {
|
||||
if cerrdefs.IsNotFound(err) || cerrdefs.IsConflict(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -137,7 +150,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
})
|
||||
return nil
|
||||
case event := <-kEvents:
|
||||
formatter.KeyboardManager.HandleKeyEvents(event, ctx, doneCh, project, options)
|
||||
navigationMenu.HandleKeyEvents(ctx, event, project, options)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -157,15 +170,11 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
return err
|
||||
})
|
||||
|
||||
if options.Start.Watch && !options.Start.NavigationMenu {
|
||||
eg.Go(func() error {
|
||||
buildOpts := *options.Create.Build
|
||||
buildOpts.Quiet = true
|
||||
return s.watch(ctx, doneCh, project, options.Start.Services, api.WatchOptions{
|
||||
Build: &buildOpts,
|
||||
LogTo: options.Start.Attach,
|
||||
})
|
||||
})
|
||||
if options.Start.Watch && watcher != nil {
|
||||
err = watcher.Start(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// We use the parent context without cancellation as we manage sigterm to stop the stack
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
gsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
@@ -44,6 +45,68 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type WatchFunc func(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error)
|
||||
|
||||
type Watcher struct {
|
||||
project *types.Project
|
||||
options api.WatchOptions
|
||||
watchFn WatchFunc
|
||||
stopFn func()
|
||||
errCh chan error
|
||||
}
|
||||
|
||||
func NewWatcher(project *types.Project, options api.UpOptions, w WatchFunc, 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: consumer,
|
||||
Build: build,
|
||||
},
|
||||
watchFn: w,
|
||||
errCh: make(chan error),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
// none of the services is eligible to watch
|
||||
return nil, fmt.Errorf("none of the selected services is configured for watch, see https://docs.docker.com/compose/how-tos/file-watch/")
|
||||
}
|
||||
|
||||
// ensure state changes are atomic
|
||||
var mx gsync.Mutex
|
||||
|
||||
func (w *Watcher) Start(ctx context.Context) error {
|
||||
mx.Lock()
|
||||
defer mx.Unlock()
|
||||
ctx, cancelFunc := context.WithCancel(ctx)
|
||||
w.stopFn = cancelFunc
|
||||
wait, err := w.watchFn(ctx, w.project, w.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
w.errCh <- wait()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Watcher) Stop() error {
|
||||
mx.Lock()
|
||||
defer mx.Unlock()
|
||||
if w.stopFn == nil {
|
||||
return nil
|
||||
}
|
||||
w.stopFn()
|
||||
w.stopFn = nil
|
||||
err := <-w.errCh
|
||||
return err
|
||||
}
|
||||
|
||||
// getSyncImplementation returns an appropriate sync implementation for the
|
||||
// project.
|
||||
//
|
||||
@@ -63,20 +126,12 @@ func (s *composeService) getSyncImplementation(project *types.Project) (sync.Syn
|
||||
return sync.NewTar(project.Name, tarDockerClient{s: s}), nil
|
||||
}
|
||||
|
||||
func (s *composeService) shouldWatch(project *types.Project) bool {
|
||||
var shouldWatch bool
|
||||
for i := range project.Services {
|
||||
service := project.Services[i]
|
||||
|
||||
if service.Develop != nil && service.Develop.Watch != nil {
|
||||
shouldWatch = true
|
||||
}
|
||||
func (s *composeService) Watch(ctx context.Context, project *types.Project, options api.WatchOptions) error {
|
||||
wait, err := s.watch(ctx, project, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return shouldWatch
|
||||
}
|
||||
|
||||
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
|
||||
return s.watch(ctx, nil, project, services, options)
|
||||
return wait()
|
||||
}
|
||||
|
||||
type watchRule struct {
|
||||
@@ -127,14 +182,14 @@ func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *composeService) watch(ctx context.Context, syncChannel chan bool, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
|
||||
func (s *composeService) watch(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error) { //nolint: gocyclo
|
||||
var err error
|
||||
if project, err = project.WithSelectedServices(services); err != nil {
|
||||
return err
|
||||
if project, err = project.WithSelectedServices(options.Services); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
syncer, err := s.getSyncImplementation(project)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
options.LogTo.Register(api.WatchLogger)
|
||||
@@ -146,7 +201,7 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
|
||||
for serviceName, service := range project.Services {
|
||||
config, err := loadDevelopmentConfig(service, project)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if service.Develop != nil {
|
||||
@@ -160,10 +215,10 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
|
||||
for _, trigger := range config.Watch {
|
||||
if trigger.Action == types.WatchActionRebuild {
|
||||
if service.Build == nil {
|
||||
return fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild)
|
||||
return nil, fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild)
|
||||
}
|
||||
if options.Build == nil {
|
||||
return fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name)
|
||||
return nil, fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name)
|
||||
}
|
||||
// set the service to always be built - watch triggers `Up()` when it receives a rebuild event
|
||||
service.PullPolicy = types.PullPolicyBuild
|
||||
@@ -179,10 +234,10 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
|
||||
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 err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,45 +246,37 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
|
||||
|
||||
serviceWatchRules, err := getWatchRules(config, service)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
rules = append(rules, serviceWatchRules...)
|
||||
}
|
||||
|
||||
if len(paths) == 0 {
|
||||
return fmt.Errorf("none of the selected services is configured for watch, consider setting an 'develop' section")
|
||||
return nil, fmt.Errorf("none of the selected services is configured for watch, consider setting a 'develop' section")
|
||||
}
|
||||
|
||||
watcher, err := watch.NewWatcher(paths)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = watcher.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := watcher.Close(); err != nil {
|
||||
logrus.Debugf("Error closing watcher: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
eg.Go(func() error {
|
||||
return s.watchEvents(ctx, project, options, watcher, syncer, rules)
|
||||
})
|
||||
options.LogTo.Log(api.WatchLogger, "Watch enabled")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return eg.Wait()
|
||||
case <-syncChannel:
|
||||
options.LogTo.Log(api.WatchLogger, "Watch disabled")
|
||||
return nil
|
||||
return func() error {
|
||||
err := eg.Wait()
|
||||
if werr := watcher.Close(); werr != nil {
|
||||
logrus.Debugf("Error closing Watcher: %v", werr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]watchRule, error) {
|
||||
@@ -295,8 +342,13 @@ func (s *composeService) watchEvents(ctx context.Context, project *types.Project
|
||||
case <-ctx.Done():
|
||||
options.LogTo.Log(api.WatchLogger, "Watch disabled")
|
||||
return nil
|
||||
case err := <-watcher.Errors():
|
||||
options.LogTo.Err(api.WatchLogger, "Watch disabled with errors")
|
||||
case err, open := <-watcher.Errors():
|
||||
if err != nil {
|
||||
options.LogTo.Err(api.WatchLogger, "Watch disabled with errors: "+err.Error())
|
||||
}
|
||||
if open {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
case batch := <-batchEvents:
|
||||
start := time.Now()
|
||||
@@ -343,7 +395,7 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
|
||||
return nil, fmt.Errorf("service %s doesn't have a build section, can't apply %s on watch", types.WatchActionRebuild, service.Name)
|
||||
}
|
||||
if trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 {
|
||||
return nil, fmt.Errorf("can't watch with action %q on service %s 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
|
||||
@@ -578,7 +630,7 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se
|
||||
return err
|
||||
}
|
||||
|
||||
p, err := project.WithSelectedServices(services)
|
||||
p, err := project.WithSelectedServices(services, types.IncludeDependents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -761,7 +813,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
|
||||
|
||||
59
pkg/e2e/bridge_test.go
Normal file
59
pkg/e2e/bridge_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestConvertAndTransformList(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
const projectName = "bridge"
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
t.Run("kubernetes manifests", func(t *testing.T) {
|
||||
kubedir := filepath.Join(tmpDir, "kubernetes")
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/bridge/compose.yaml", "--project-name", projectName, "bridge", "convert",
|
||||
"--output", kubedir)
|
||||
assert.NilError(t, res.Error)
|
||||
assert.Equal(t, res.ExitCode, 0)
|
||||
res = c.RunCmd(t, "diff", "-r", kubedir, "./fixtures/bridge/expected-kubernetes")
|
||||
assert.NilError(t, res.Error, res.Combined())
|
||||
})
|
||||
|
||||
t.Run("helm charts", func(t *testing.T) {
|
||||
helmDir := filepath.Join(tmpDir, "helm")
|
||||
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/bridge/compose.yaml", "--project-name", projectName, "bridge", "convert",
|
||||
"--output", helmDir, "--transformation", "docker/compose-bridge-helm")
|
||||
assert.NilError(t, res.Error)
|
||||
assert.Equal(t, res.ExitCode, 0)
|
||||
res = c.RunCmd(t, "diff", "-r", helmDir, "./fixtures/bridge/expected-helm")
|
||||
assert.NilError(t, res.Error, res.Combined())
|
||||
})
|
||||
|
||||
t.Run("list transformers images", func(t *testing.T) {
|
||||
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "bridge", "transformations",
|
||||
"ls")
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "docker/compose-bridge-helm"), res.Combined())
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "docker/compose-bridge-kubernetes"), res.Combined())
|
||||
})
|
||||
}
|
||||
@@ -284,7 +284,7 @@ func TestBuildImageDependencies(t *testing.T) {
|
||||
|
||||
t.Run("BuildKit by dependency order", func(t *testing.T) {
|
||||
cli := NewCLI(t, WithEnv(
|
||||
"DOCKER_BUILDKIT=1",
|
||||
"DOCKER_BUILDKIT=1", "COMPOSE_BAKE=0",
|
||||
"COMPOSE_FILE=./fixtures/build-dependencies/classic.yaml",
|
||||
))
|
||||
doTest(t, cli, "build")
|
||||
@@ -293,7 +293,7 @@ func TestBuildImageDependencies(t *testing.T) {
|
||||
|
||||
t.Run("BuildKit by additional contexts", func(t *testing.T) {
|
||||
cli := NewCLI(t, WithEnv(
|
||||
"DOCKER_BUILDKIT=1",
|
||||
"DOCKER_BUILDKIT=1", "COMPOSE_BAKE=0",
|
||||
"COMPOSE_FILE=./fixtures/build-dependencies/compose.yaml",
|
||||
))
|
||||
doTest(t, cli, "build")
|
||||
@@ -524,3 +524,87 @@ func TestBuildEntitlements(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildDependsOn(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "down", "--rmi=local")
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "--progress=plain", "up", "test2")
|
||||
out := res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "test1 Built"))
|
||||
}
|
||||
|
||||
func TestBuildSubset(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "down", "--rmi=local")
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "build", "main")
|
||||
out := res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "main Built"))
|
||||
}
|
||||
|
||||
func TestBuildDependentImage(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "down", "--rmi=local")
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "firstbuild")
|
||||
out := res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "firstbuild Built"))
|
||||
|
||||
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "secondbuild")
|
||||
out = res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "secondbuild Built"))
|
||||
}
|
||||
|
||||
func TestBuildSubDependencies(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
|
||||
t.Cleanup(func() {
|
||||
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "down", "--rmi=local")
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "build", "main")
|
||||
out := res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "main Built"))
|
||||
|
||||
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "up", "--build", "main")
|
||||
out = res.Combined()
|
||||
assert.Check(t, strings.Contains(out, "main Built"))
|
||||
}
|
||||
|
||||
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"})
|
||||
})
|
||||
}
|
||||
|
||||
18
pkg/e2e/fixtures/bridge/Dockerfile
Normal file
18
pkg/e2e/fixtures/bridge/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM alpine
|
||||
ENV ENV_FROM_DOCKERFILE=1
|
||||
EXPOSE 8081
|
||||
CMD ["echo", "Hello from Dockerfile"]
|
||||
31
pkg/e2e/fixtures/bridge/compose.yaml
Normal file
31
pkg/e2e/fixtures/bridge/compose.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
services:
|
||||
serviceA:
|
||||
image: alpine
|
||||
build: .
|
||||
ports:
|
||||
- 80:8080
|
||||
networks:
|
||||
- private-network
|
||||
configs:
|
||||
- source: my-config
|
||||
target: /etc/my-config1.txt
|
||||
serviceB:
|
||||
image: alpine
|
||||
build: .
|
||||
ports:
|
||||
- 8081:8082
|
||||
secrets:
|
||||
- my-secrets
|
||||
networks:
|
||||
- private-network
|
||||
- public-network
|
||||
configs:
|
||||
my-config:
|
||||
file: my-config.txt
|
||||
secrets:
|
||||
my-secrets:
|
||||
file: not-so-secret.txt
|
||||
networks:
|
||||
private-network:
|
||||
internal: true
|
||||
public-network: {}
|
||||
12
pkg/e2e/fixtures/bridge/expected-helm/Chart.yaml
Executable file
12
pkg/e2e/fixtures/bridge/expected-helm/Chart.yaml
Executable file
@@ -0,0 +1,12 @@
|
||||
#! Chart.yaml
|
||||
apiVersion: v2
|
||||
name: bridge
|
||||
version: 0.0.1
|
||||
# kubeVersion: >= 1.29.1
|
||||
description: A generated Helm Chart for bridge generated via compose-bridge.
|
||||
type: application
|
||||
keywords:
|
||||
- bridge
|
||||
appVersion: 'v0.0.1'
|
||||
sources:
|
||||
annotations:
|
||||
8
pkg/e2e/fixtures/bridge/expected-helm/templates/0-bridge-namespace.yaml
Executable file
8
pkg/e2e/fixtures/bridge/expected-helm/templates/0-bridge-namespace.yaml
Executable file
@@ -0,0 +1,8 @@
|
||||
#! 0-bridge-namespace.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: bridge
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
12
pkg/e2e/fixtures/bridge/expected-helm/templates/bridge-configs.yaml
Executable file
12
pkg/e2e/fixtures/bridge/expected-helm/templates/bridge-configs.yaml
Executable file
@@ -0,0 +1,12 @@
|
||||
#! bridge-configs.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: bridge
|
||||
namespace: bridge
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
data:
|
||||
my-config: |
|
||||
My config file
|
||||
13
pkg/e2e/fixtures/bridge/expected-helm/templates/my-secrets-secret.yaml
Executable file
13
pkg/e2e/fixtures/bridge/expected-helm/templates/my-secrets-secret.yaml
Executable file
@@ -0,0 +1,13 @@
|
||||
#! my-secrets-secret.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: my-secrets
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.secret: my-secrets
|
||||
data:
|
||||
my-secrets: bm90LXNlY3JldA==
|
||||
type: Opaque
|
||||
@@ -0,0 +1,24 @@
|
||||
#! private-network-network-policy.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: private-network-network-policy
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
com.docker.compose.network.private-network: "true"
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
com.docker.compose.network.private-network: "true"
|
||||
egress:
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
com.docker.compose.network.private-network: "true"
|
||||
@@ -0,0 +1,24 @@
|
||||
#! public-network-network-policy.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: public-network-network-policy
|
||||
namespace: {{ .Values.namespace }}
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
com.docker.compose.network.public-network: "true"
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
com.docker.compose.network.public-network: "true"
|
||||
egress:
|
||||
- to:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
com.docker.compose.network.public-network: "true"
|
||||
45
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-deployment.yaml
Executable file
45
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-deployment.yaml
Executable file
@@ -0,0 +1,45 @@
|
||||
#! serviceA-deployment.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: servicea
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
com.docker.compose.network.private-network: "true"
|
||||
spec:
|
||||
containers:
|
||||
- name: servicea
|
||||
image: {{ .Values.serviceA.image }}
|
||||
imagePullPolicy: {{ .Values.serviceA.imagePullPolicy }}
|
||||
ports:
|
||||
- name: servicea-8080
|
||||
containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: etc-my-config1-txt
|
||||
mountPath: /etc/my-config1.txt
|
||||
subPath: my-config
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: etc-my-config1-txt
|
||||
configMap:
|
||||
name: bridge
|
||||
items:
|
||||
- key: my-config
|
||||
path: my-config
|
||||
19
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-expose.yaml
Executable file
19
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-expose.yaml
Executable file
@@ -0,0 +1,19 @@
|
||||
#! serviceA-expose.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: servicea
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
selector:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
ports:
|
||||
- name: servicea-8080
|
||||
port: 8080
|
||||
targetPort: servicea-8080
|
||||
25
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-service.yaml
Executable file
25
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceA-service.yaml
Executable file
@@ -0,0 +1,25 @@
|
||||
# check if there is at least one published port
|
||||
|
||||
#! serviceA-service.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: servicea-published
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceA
|
||||
ports:
|
||||
- name: servicea-80
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: servicea-8080
|
||||
|
||||
# check if there is at least one published port
|
||||
46
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-deployment.yaml
Executable file
46
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-deployment.yaml
Executable file
@@ -0,0 +1,46 @@
|
||||
#! serviceB-deployment.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: serviceb
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
com.docker.compose.network.private-network: "true"
|
||||
com.docker.compose.network.public-network: "true"
|
||||
spec:
|
||||
containers:
|
||||
- name: serviceb
|
||||
image: {{ .Values.serviceB.image }}
|
||||
imagePullPolicy: {{ .Values.serviceB.imagePullPolicy }}
|
||||
ports:
|
||||
- name: serviceb-8082
|
||||
containerPort: 8082
|
||||
volumeMounts:
|
||||
- name: run-secrets-my-secrets
|
||||
mountPath: /run/secrets/my-secrets
|
||||
subPath: my-secrets
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: run-secrets-my-secrets
|
||||
secret:
|
||||
secretName: my-secrets
|
||||
items:
|
||||
- key: my-secrets
|
||||
path: my-secrets
|
||||
19
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-expose.yaml
Executable file
19
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-expose.yaml
Executable file
@@ -0,0 +1,19 @@
|
||||
#! serviceB-expose.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: serviceb
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
selector:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
ports:
|
||||
- name: serviceb-8082
|
||||
port: 8082
|
||||
targetPort: serviceb-8082
|
||||
21
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-service.yaml
Executable file
21
pkg/e2e/fixtures/bridge/expected-helm/templates/serviceB-service.yaml
Executable file
@@ -0,0 +1,21 @@
|
||||
#! serviceB-service.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: serviceb-published
|
||||
namespace: {{ .Values.namespace }}
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
com.docker.compose.project: bridge
|
||||
com.docker.compose.service: serviceB
|
||||
ports:
|
||||
- name: serviceb-8081
|
||||
port: 8081
|
||||
protocol: TCP
|
||||
targetPort: serviceb-8082
|
||||
13
pkg/e2e/fixtures/bridge/expected-helm/values.yaml
Executable file
13
pkg/e2e/fixtures/bridge/expected-helm/values.yaml
Executable file
@@ -0,0 +1,13 @@
|
||||
#! values.yaml
|
||||
# Namespace
|
||||
namespace: bridge
|
||||
# Services variables
|
||||
|
||||
serviceA:
|
||||
image: alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
serviceB:
|
||||
image: alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
# You can apply the same logic to loop on networks, volumes, secrets and configs...
|
||||
8
pkg/e2e/fixtures/bridge/expected-kubernetes/base/0-bridge-namespace.yaml
Executable file
8
pkg/e2e/fixtures/bridge/expected-kubernetes/base/0-bridge-namespace.yaml
Executable file
@@ -0,0 +1,8 @@
|
||||
#! 0-bridge-namespace.yaml
|
||||
# Generated code, do not edit
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: bridge
|
||||
labels:
|
||||
com.docker.compose.project: bridge
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user