Compare commits

...

49 Commits

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
2025-05-26 16:37:45 +02:00
109 changed files with 2723 additions and 797 deletions

View File

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

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

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

View File

@@ -27,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")

View File

@@ -47,7 +47,6 @@ import (
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/remote"
"github.com/docker/compose/v2/pkg/utils"
buildkit "github.com/moby/buildkit/util/progress/progressui"
"github.com/morikuni/aec"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -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,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli
watchCommand(&opts, dockerCli, backend),
publishCommand(&opts, dockerCli, backend),
alphaCommand(&opts, dockerCli, backend),
bridgeCommand(&opts, dockerCli),
)
c.Flags().SetInterspersed(false)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -261,6 +261,7 @@ func runUp(
return err
}
bo.Services = services
bo.Deps = !upOptions.noDeps
build = &bo
}

View File

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

View File

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

View File

@@ -17,11 +17,13 @@
package main
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func main() {
@@ -43,16 +45,27 @@ func composeCommand() *cobra.Command {
TraverseChildren: true,
}
c.PersistentFlags().String("project-name", "", "compose project name") // unused
c.AddCommand(&cobra.Command{
upCmd := &cobra.Command{
Use: "up",
Run: up,
Args: cobra.ExactArgs(1),
})
c.AddCommand(&cobra.Command{
}
upCmd.Flags().String("type", "", "Database type (mysql, postgres, etc.)")
_ = upCmd.MarkFlagRequired("type")
upCmd.Flags().Int("size", 10, "Database size in GB")
upCmd.Flags().String("name", "", "Name of the database to be created")
_ = upCmd.MarkFlagRequired("name")
downCmd := &cobra.Command{
Use: "down",
Run: down,
Args: cobra.ExactArgs(1),
})
}
downCmd.Flags().String("name", "", "Name of the database to be deleted")
_ = downCmd.MarkFlagRequired("name")
c.AddCommand(upCmd, downCmd)
c.AddCommand(metadataCommand(upCmd, downCmd))
return c
}
@@ -72,3 +85,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"`
}

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
command: docker compose bridge
short: Convert compose files into another model
long: Convert compose files into another model
pname: docker compose
plink: docker_compose.yaml
cname:
- docker compose bridge convert
- docker compose bridge transformations
clink:
- docker_compose_bridge_convert.yaml
- docker_compose_bridge_transformations.yaml
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

View File

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

View File

@@ -0,0 +1,29 @@
command: docker compose bridge transformations
short: Manage transformation images
long: Manage transformation images
pname: docker compose bridge
plink: docker_compose_bridge.yaml
cname:
- docker compose bridge transformations create
- docker compose bridge transformations list
clink:
- docker_compose_bridge_transformations_create.yaml
- docker_compose_bridge_transformations_list.yaml
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

View File

@@ -0,0 +1,36 @@
command: docker compose bridge transformations create
short: Create a new transformation
long: Create a new transformation
usage: docker compose bridge transformations create [OPTION] PATH
pname: docker compose bridge transformations
plink: docker_compose_bridge_transformations.yaml
options:
- option: from
shorthand: f
value_type: string
description: |
Existing transformation to copy (default: docker/compose-bridge-kubernetes)
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
inherited_options:
- option: dry-run
value_type: bool
default_value: "false"
description: Execute command in dry run mode
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false
deprecated: false
hidden: false
experimental: false
experimentalcli: false
kubernetes: false
swarm: false

View File

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

View File

@@ -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

38
go.mod
View File

@@ -8,15 +8,16 @@ require (
github.com/Microsoft/go-winio v0.6.2
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/buger/goterm v1.0.4
github.com/compose-spec/compose-go/v2 v2.6.4
github.com/containerd/containerd/v2 v2.1.1
github.com/compose-spec/compose-go/v2 v2.6.5
github.com/containerd/containerd/v2 v2.1.2
github.com/containerd/errdefs v1.0.0
github.com/containerd/platforms v1.0.0-rc.1
github.com/davecgh/go-spew v1.1.1
github.com/distribution/reference v0.6.0
github.com/docker/buildx v0.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.2.2+incompatible
github.com/docker/cli-docs-tool v0.10.0
github.com/docker/docker v28.2.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
@@ -28,7 +29,7 @@ require (
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-ps v1.0.0
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/buildkit v0.22.0
github.com/moby/buildkit v0.23.0
github.com/moby/go-archive v0.1.0
github.com/moby/patternmatcher v0.6.0
github.com/moby/sys/atomicwriter v0.1.0
@@ -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
@@ -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

82
go.sum
View File

@@ -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.6.5 h1:H7xP5OMKdkN2p0brx01slxIU6dE/q6ybbG+jozPtIqk=
github.com/compose-spec/compose-go/v2 v2.6.5/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/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.2 h1:4ZQxB+FVYmwXZgpBcKfar6ieppm3KC5C6FRKvtJ6DRU=
github.com/containerd/containerd/v2 v2.1.2/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -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.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.1.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.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@@ -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.0 h1:HV+u7xM2IZhAjVautFR2l5FNhkxFR0jhF5ILXyc3398=
github.com/moby/buildkit v0.23.0/go.mod h1:v5jMDvQgUyidk3wu3NvVAAd5JJo83nfet9Gf/o0+EAQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
@@ -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=

View File

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

View File

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

View File

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

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

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

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

@@ -0,0 +1,118 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package bridge
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/moby/go-archive"
)
const (
TransformerLabel = "com.docker.compose.bridge"
DefaultTransformerImage = "docker/compose-bridge-kubernetes"
)
type CreateTransformerOptions struct {
Dest string
From string
}
func CreateTransformer(ctx context.Context, dockerCli command.Cli, options CreateTransformerOptions) error {
if options.From == "" {
options.From = DefaultTransformerImage
}
out, err := filepath.Abs(options.Dest)
if err != nil {
return err
}
if _, err := os.Stat(out); err == nil {
return fmt.Errorf("output folder %s already exists", out)
}
tmpl := filepath.Join(out, "templates")
err = os.MkdirAll(tmpl, 0o744)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("cannot create output folder: %w", err)
}
if err := command.ValidateOutputPath(out); err != nil {
return err
}
created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
Image: options.From,
}, &container.HostConfig{}, &network.NetworkingConfig{}, nil, "")
defer func() {
_ = dockerCli.Client().ContainerRemove(context.Background(), created.ID, container.RemoveOptions{Force: true})
}()
if err != nil {
return err
}
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, "/templates")
if err != nil {
return err
}
defer func() {
_ = content.Close()
}()
srcInfo := archive.CopyInfo{
Path: "/templates",
Exists: true,
IsDir: stat.Mode.IsDir(),
}
preArchive := content
if srcInfo.RebaseName != "" {
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
}
if err := archive.CopyTo(preArchive, srcInfo, out); err != nil {
return err
}
dockerfile := `FROM docker/compose-bridge-transformer
LABEL com.docker.compose.bridge=transformation
COPY templates /templates
`
if err := os.WriteFile(filepath.Join(out, "Dockerfile"), []byte(dockerfile), 0o700); err != nil {
return err
}
_, err = fmt.Fprintf(dockerCli.Out(), "Transformer created in %q\n", out)
return err
}
func ListTransformers(ctx context.Context, dockerCli command.Cli) ([]image.Summary, error) {
api := dockerCli.Client()
return api.ImageList(ctx, image.ListOptions{
Filters: filters.NewArgs(
filters.Arg("label", fmt.Sprintf("%s=%s", TransformerLabel, "transformation")),
),
})
}

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

@@ -0,0 +1,90 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"bytes"
"fmt"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/config/configfile"
)
// --use-api-socket is not actually supported by the Docker Engine
// but is a client-side hack (see https://github.com/docker/cli/blob/master/cli/command/container/create.go#L246)
// we replicate here by transforming the project model
func (s *composeService) useAPISocket(project *types.Project) (*types.Project, error) {
useAPISocket := false
for _, service := range project.Services {
if service.UseAPISocket {
useAPISocket = true
break
}
}
if !useAPISocket {
return project, nil
}
socket := s.dockerCli.DockerEndpoint().Host
if !strings.HasPrefix(socket, "unix://") {
return nil, fmt.Errorf("use_api_socket can only be used with unix sockets: docker endpoint %s is incompatible", socket)
}
socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
creds, err := s.dockerCli.ConfigFile().GetAllCredentials()
if err != nil {
return nil, fmt.Errorf("resolving credentials failed: %w", err)
}
newConfig := &configfile.ConfigFile{
AuthConfigs: creds,
}
var configBuf bytes.Buffer
if err := newConfig.SaveToWriter(&configBuf); err != nil {
return nil, fmt.Errorf("saving creds for API socket: %w", err)
}
project.Configs["#apisocket"] = types.ConfigObjConfig{
Content: configBuf.String(),
}
for name, service := range project.Services {
service.Volumes = append(service.Volumes, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind,
Source: socket,
Target: "/var/run/docker.sock",
})
_, envvarPresent := service.Environment["DOCKER_CONFIG"]
// If the DOCKER_CONFIG env var is already present, we assume the client knows
// what they're doing and don't inject the creds.
if !envvarPresent {
// Set our special little location for the config file.
path := "/run/secrets/docker"
service.Environment["DOCKER_CONFIG"] = &path
}
service.Configs = append(service.Configs, types.ServiceConfigObjConfig{
Source: "#apisocket",
Target: "/run/secrets/docker/config.json",
})
project.Services[name] = service
}
return project, nil
}

View File

@@ -22,19 +22,16 @@ import (
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/platforms"
"github.com/docker/buildx/build"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/controller/pb"
"github.com/docker/buildx/store/storeutil"
"github.com/docker/buildx/util/buildflags"
xprogress "github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/hints"
cliopts "github.com/docker/cli/opts"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
@@ -71,10 +68,6 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
}, s.stdinfo(), "Building")
}
const bakeSuggest = "Compose can now delegate builds to bake for better performance.\n To do so, set COMPOSE_BAKE=true."
var suggest sync.Once
//nolint:gocyclo
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]api.ImageSummary) (map[string]string, error) {
imageIDs := map[string]string{}
@@ -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")
@@ -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 {

View File

@@ -23,6 +23,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"os"
"os/exec"
"path/filepath"
@@ -51,12 +52,7 @@ import (
func buildWithBake(dockerCli command.Cli) (bool, error) {
b, ok := os.LookupEnv("COMPOSE_BAKE")
if !ok {
if dockerCli.ConfigFile().Plugins["compose"]["build"] == "bake" {
b, ok = "true", true
}
}
if !ok {
return false, nil
b = "true"
}
bake, err := strconv.ParseBool(b)
if err != nil {
@@ -72,6 +68,7 @@ func buildWithBake(dockerCli command.Cli) (bool, error) {
}
if !enabled {
logrus.Warnf("Docker Compose is configured to build using Bake, but buildkit isn't enabled")
return false, nil
}
_, err = manager.GetPlugin("buildx", dockerCli, &cobra.Command{})
@@ -117,6 +114,7 @@ type bakeTarget struct {
Ulimits []string `json:"ulimits,omitempty"`
Call string `json:"call,omitempty"`
Entitlements []string `json:"entitlements,omitempty"`
ExtraHosts map[string]string `json:"extra-hosts,omitempty"`
Outputs []string `json:"output,omitempty"`
}
@@ -144,13 +142,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 +174,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,14 +203,15 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
}
}
cfg.Targets[serviceName] = bakeTarget{
target := targets[serviceName]
cfg.Targets[target] = bakeTarget{
Context: build.Context,
Contexts: additionalContexts(build.AdditionalContexts),
Contexts: additionalContexts(build.AdditionalContexts, targets),
Dockerfile: dockerFilePath(build.Context, build.Dockerfile),
DockerfileInline: strings.ReplaceAll(build.DockerfileInline, "${", "$${"),
Args: args,
Labels: build.Labels,
Tags: append(build.Tags, image),
Tags: append(build.Tags, api.GetImageNameOrDefault(service, project.Name)),
CacheFrom: build.CacheFrom,
// CacheTo: TODO
@@ -216,11 +224,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 +252,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
@@ -273,9 +297,8 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
// Use docker/cli mechanism to propagate termination signal to child process
server, err := socket.NewPluginServer(nil)
if err != nil {
if err == nil {
defer server.Close() //nolint:errcheck
cmd.Cancel = server.Close
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
}
@@ -327,7 +350,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 +363,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 +375,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
}
@@ -420,8 +452,15 @@ func dockerFilePath(ctxName string, dockerfile string) string {
if dockerfile == "" {
return ""
}
if urlutil.IsGitURL(ctxName) || filepath.IsAbs(dockerfile) {
if urlutil.IsGitURL(ctxName) {
return dockerfile
}
return filepath.Join(ctxName, dockerfile)
if !filepath.IsAbs(dockerfile) {
dockerfile = filepath.Join(ctxName, dockerfile)
}
symlinks, err := filepath.EvalSymlinks(dockerfile)
if err == nil {
return symlinks
}
return dockerfile
}

View File

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

View File

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

View File

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

View File

@@ -101,7 +101,7 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options
return tracing.SpanWrapFunc("service/apply", tracing.ServiceOptions(service), func(ctx context.Context) error {
strategy := options.RecreateDependencies
if utils.StringContains(options.Services, name) {
if slices.Contains(options.Services, name) {
strategy = options.Recreate
}
return c.ensureService(ctx, project, service, strategy, options.Inherit, options.Timeout)
@@ -191,7 +191,6 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
case ContainerCreated:
case ContainerRestarting:
case ContainerExited:
w.Event(progress.CreatedEvent(name))
default:
container := container
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "service/start", tracing.ContainerOptions(container), func(ctx context.Context) error {

View File

@@ -24,15 +24,13 @@ 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"
@@ -41,10 +39,13 @@ import (
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/versions"
volumetypes "github.com/docker/docker/api/types/volume"
"github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat"
"github.com/sirupsen/logrus"
cdi "tags.cncf.io/container-device-interface/pkg/parser"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/prompt"
)
type createOptions struct {
@@ -113,6 +114,13 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
"--remove-orphans flag to clean it up.", orphans.names())
}
}
// Temporary implementation of use_api_socket until we get actual support inside docker engine
project, err = s.useAPISocket(project)
if err != nil {
return err
}
return newConvergence(options.Services, observedState, networks, volumes, s).apply(ctx, project, options)
}
@@ -1253,7 +1261,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
}
id, err := s.resolveOrCreateNetwork(ctx, project, name, n)
if errdefs.IsConflict(err) {
if cerrdefs.IsConflict(err) {
// Maybe another execution of `docker compose up|run` created same network
// let's retry once
return s.resolveOrCreateNetwork(ctx, project, name, n)
@@ -1262,6 +1270,9 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
}
func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (string, error) { //nolint:gocyclo
// This is containers that could be left after a diverged network was removed
var dangledContainers Containers
// First, try to find a unique network matching by name or ID
inspect, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
if err == nil {
@@ -1295,7 +1306,7 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
return inspect.ID, nil
}
err = s.removeDivergedNetwork(ctx, project, name, n)
dangledContainers, err = s.removeDivergedNetwork(ctx, project, name, n)
if err != nil {
return "", err
}
@@ -1312,8 +1323,8 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
}
// NetworkList Matches all or part of a network name, so we have to filter for a strict match
networks = utils.Filter(networks, func(net network.Summary) bool {
return net.Name == n.Name
networks = slices.DeleteFunc(networks, func(net network.Summary) bool {
return net.Name != n.Name
})
for _, net := range networks {
@@ -1392,10 +1403,16 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
return "", fmt.Errorf("failed to create network %s: %w", n.Name, err)
}
w.Event(progress.CreatedEvent(networkEventName))
err = s.connectNetwork(ctx, n.Name, dangledContainers, nil)
if err != nil {
return "", err
}
return resp.ID, nil
}
func (s *composeService) removeDivergedNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) error {
func (s *composeService) removeDivergedNetwork(ctx context.Context, project *types.Project, name string, n *types.NetworkConfig) (Containers, error) {
// Remove services attached to this network to force recreation
var services []string
for _, service := range project.Services.Filter(func(config types.ServiceConfig) bool {
@@ -1412,13 +1429,54 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
Project: project,
})
if err != nil {
return err
return nil, err
}
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, true, services...)
if err != nil {
return nil, err
}
err = s.disconnectNetwork(ctx, n.Name, containers)
if err != nil {
return nil, err
}
err = s.apiClient().NetworkRemove(ctx, n.Name)
eventName := fmt.Sprintf("Network %s", n.Name)
progress.ContextWriter(ctx).Event(progress.RemovedEvent(eventName))
return err
return containers, err
}
func (s *composeService) disconnectNetwork(
ctx context.Context,
network string,
containers Containers,
) error {
for _, c := range containers {
err := s.apiClient().NetworkDisconnect(ctx, network, c.ID, true)
if err != nil {
return err
}
}
return nil
}
func (s *composeService) connectNetwork(
ctx context.Context,
network string,
containers Containers,
config *network.EndpointSettings,
) error {
for _, c := range containers {
err := s.apiClient().NetworkConnect(ctx, network, c.ID, config)
if err != nil {
return err
}
}
return nil
}
func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.NetworkConfig) (string, error) {
@@ -1436,18 +1494,19 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
if len(networks) == 0 {
// in this instance, n.Name is really an ID
sn, err := s.apiClient().NetworkInspect(ctx, n.Name, network.InspectOptions{})
if err != nil && !errdefs.IsNotFound(err) {
if err == nil {
networks = append(networks, sn)
} else if !cerrdefs.IsNotFound(err) {
return "", err
}
networks = append(networks, sn)
}
// NetworkList API doesn't return the exact name match, so we can retrieve more than one network with a request
networks = utils.Filter(networks, func(net network.Inspect) bool {
// later in this function, the name is changed the to ID.
networks = slices.DeleteFunc(networks, func(net network.Inspect) bool {
// this function is called during the rebuild stage of `compose watch`.
// we still require just one network back, but we need to run the search on the ID
return net.Name == n.Name || net.ID == n.Name
return net.Name != n.Name && net.ID != n.Name
})
switch len(networks) {
@@ -1474,7 +1533,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project, assumeYes bool) (string, error) {
inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
if err != nil {
if !errdefs.IsNotFound(err) {
if !cerrdefs.IsNotFound(err) {
return "", err
}
if volume.External {

View File

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

View File

@@ -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
}
@@ -345,7 +345,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
w := progress.ContextWriter(ctx)
eventName := getContainerProgressName(ctr)
err := s.stopContainer(ctx, w, service, ctr, timeout)
if errdefs.IsNotFound(err) {
if cerrdefs.IsNotFound(err) {
w.Event(progress.RemovedEvent(eventName))
return nil
}
@@ -357,7 +357,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
Force: true,
RemoveVolumes: volumes,
})
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
if err != nil && !cerrdefs.IsNotFound(err) && !cerrdefs.IsConflict(err) {
w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
return err
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@
package compose
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -24,11 +25,13 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/config"
"github.com/docker/compose/v2/pkg/progress"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -42,10 +45,11 @@ type JsonMessage struct {
}
const (
ErrorType = "error"
InfoType = "info"
SetEnvType = "setenv"
DebugType = "debug"
ErrorType = "error"
InfoType = "info"
SetEnvType = "setenv"
DebugType = "debug"
providerMetadataDirectory = "compose/providers"
)
func (s *composeService) runPlugin(ctx context.Context, project *types.Project, service types.ServiceConfig, command string) error {
@@ -56,7 +60,10 @@ 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 {
@@ -160,13 +167,27 @@ 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)
var currentCommandMetadata CommandMetadata
switch command {
case "up":
currentCommandMetadata = cmdOptionsMetadata.Up
case "down":
currentCommandMetadata = cmdOptionsMetadata.Down
}
commandMetadataIsEmpty := len(currentCommandMetadata.Parameters) == 0
provider := *service.Provider
if err := currentCommandMetadata.CheckRequiredParameters(provider); !commandMetadataIsEmpty && err != nil {
return nil, err
}
args := []string{"compose", "--project-name", project.Name, command}
for k, v := range provider.Options {
for _, value := range v {
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)
@@ -186,7 +207,6 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
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())
}
@@ -196,5 +216,73 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, &carrier)
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
return cmd
return cmd, nil
}
func (s *composeService) getPluginMetadata(path, command string) ProviderMetadata {
cmd := exec.Command(path, "compose", "metadata")
stdout := &bytes.Buffer{}
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
logrus.Debugf("failed to start plugin metadata command: %v", err)
return ProviderMetadata{}
}
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"`
}
type CommandMetadata struct {
Parameters []ParameterMetadata `json:"parameters"`
}
type ParameterMetadata struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}
func (c CommandMetadata) GetParameter(paramName string) (ParameterMetadata, bool) {
for _, p := range c.Parameters {
if p.Name == paramName {
return p, true
}
}
return ParameterMetadata{}, false
}
func (c CommandMetadata) CheckRequiredParameters(provider types.ServiceProviderConfig) error {
for _, p := range c.Parameters {
if p.Required {
if _, ok := provider.Options[p.Name]; !ok {
return fmt.Errorf("required parameter %q is missing from provider %q definition", p.Name, provider.Type)
}
}
}
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -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,59 @@ func TestBuildEntitlements(t *testing.T) {
}
})
}
func TestBuildDependsOn(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "--progress=plain", "up", "test2")
out := res.Combined()
assert.Check(t, strings.Contains(out, "test1 Built"))
}
func TestBuildSubset(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "build", "main")
out := res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
}
func TestBuildDependentImage(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "firstbuild")
out := res.Combined()
assert.Check(t, strings.Contains(out, "firstbuild Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "secondbuild")
out = res.Combined()
assert.Check(t, strings.Contains(out, "secondbuild Built"))
}
func TestBuildSubDependencies(t *testing.T) {
c := NewParallelCLI(t)
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "down", "--rmi=local")
})
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "build", "main")
out := res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "up", "--build", "main")
out = res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
#! serviceA-deployment.yaml
# Generated code, do not edit
apiVersion: apps/v1
kind: Deployment
metadata:
name: servicea
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
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: alpine
imagePullPolicy: IfNotPresent
ports:
- name: servicea-8080
containerPort: 8080
volumeMounts:
- name: etc-my-config1-txt
mountPath: /etc/my-config1.txt
subPath: my-config
readOnly: true
volumes:
- name: etc-my-config1-txt
configMap:
name: bridge
items:
- key: my-config
path: my-config

View File

@@ -0,0 +1,18 @@
#! serviceA-expose.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: servicea
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
ports:
- name: servicea-8080
port: 8080
targetPort: servicea-8080

View File

@@ -0,0 +1,23 @@
# 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: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceA
ports:
- name: servicea-80
port: 80
protocol: TCP
targetPort: servicea-8080
# check if there is at least one published port

View File

@@ -0,0 +1,45 @@
#! serviceB-deployment.yaml
# Generated code, do not edit
apiVersion: apps/v1
kind: Deployment
metadata:
name: serviceb
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
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: alpine
imagePullPolicy: IfNotPresent
ports:
- name: serviceb-8082
containerPort: 8082
volumeMounts:
- name: run-secrets-my-secrets
mountPath: /run/secrets/my-secrets
subPath: my-secrets
readOnly: true
volumes:
- name: run-secrets-my-secrets
secret:
secretName: my-secrets
items:
- key: my-secrets
path: my-secrets

View File

@@ -0,0 +1,18 @@
#! serviceB-expose.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
ports:
- name: serviceb-8082
port: 8082
targetPort: serviceb-8082

View File

@@ -0,0 +1,19 @@
#! serviceB-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb-published
namespace: bridge
labels:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
spec:
selector:
com.docker.compose.project: bridge
com.docker.compose.service: serviceB
ports:
- name: serviceb-8081
port: 8081
protocol: TCP
targetPort: serviceb-8082

View File

@@ -0,0 +1,9 @@
#! kustomization.yaml
# Generated code, do not edit
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: serviceA-service.yaml
- path: serviceB-service.yaml

View File

@@ -0,0 +1,13 @@
# 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: bridge
spec:
type: LoadBalancer
# check if there is at least one published port

View File

@@ -0,0 +1,9 @@
#! serviceB-service.yaml
# Generated code, do not edit
apiVersion: v1
kind: Service
metadata:
name: serviceb-published
namespace: bridge
spec:
type: LoadBalancer

View File

@@ -0,0 +1 @@
My config file

View File

@@ -0,0 +1 @@
not-secret

View File

@@ -0,0 +1,15 @@
services:
test1:
pull_policy: build
build:
dockerfile_inline: FROM alpine
command:
- echo
- "test 1 success"
test2:
image: alpine
depends_on:
- test1
command:
- echo
- "test 2 success"

View File

@@ -0,0 +1,26 @@
services:
firstbuild:
build:
dockerfile_inline: |
FROM alpine
additional_contexts:
dep1: service:dep1
entrypoint: ["echo", "Hello from firstbuild"]
depends_on:
- dep1
secondbuild:
build:
dockerfile_inline: |
FROM alpine
additional_contexts:
dep1: service:dep1
entrypoint: ["echo", "Hello from secondbuild"]
depends_on:
- dep1
dep1:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from dep1"]

View File

@@ -0,0 +1,36 @@
services:
main:
build:
dockerfile_inline: |
FROM alpine
additional_contexts:
dep1: service:dep1
dep2: service:dep2
entrypoint: ["echo", "Hello from main"]
dep1:
build:
dockerfile_inline: |
FROM alpine
additional_contexts:
subdep1: service:subdep1
subdep2: service:subdep2
entrypoint: ["echo", "Hello from dep1"]
dep2:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from dep2"]
subdep1:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from subdep1"]
subdep2:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from subdep2"]

View File

@@ -0,0 +1,14 @@
services:
main:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from main"]
depends_on:
- dep1
dep1:
build:
dockerfile_inline: |
FROM alpine
entrypoint: ["echo", "Hello from dep1"]

View File

@@ -0,0 +1,10 @@
services:
web:
image: nginx
networks:
- test
networks:
test:
labels:
- foo=${FOO:-foo}

View File

@@ -199,3 +199,24 @@ func TestInterfaceName(t *testing.T) {
})
res.Assert(t, icmd.Expected{Out: "foobar@"})
}
func TestNetworkRecreate(t *testing.T) {
c := NewCLI(t)
const projectName = "network_recreate"
t.Cleanup(func() {
c.cleanupWithDown(t, projectName)
})
c.RunDockerComposeCmd(t, "-f", "./fixtures/network-recreate/compose.yaml", "--project-name", projectName, "up", "-d")
c = NewCLI(t, WithEnv("FOO=bar"))
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-recreate/compose.yaml", "--project-name", projectName, "--progress=plain", "up", "-d")
err := res.Stderr()
fmt.Println(err)
res.Assert(t, icmd.Expected{Err: `
Container network_recreate-web-1 Stopped
Network network_recreate_test Removed
Network network_recreate_test Creating
Network network_recreate_test Created
Container network_recreate-web-1 Starting
Container network_recreate-web-1 Started`})
}

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