Compare commits

...

28 Commits

Author SHA1 Message Date
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
105 changed files with 2305 additions and 667 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)
}
}
@@ -86,13 +88,10 @@ func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service,
return nil
}
sort.Slice(images, func(i, j int) bool {
return images[i].ContainerName < images[j].ContainerName
})
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 +102,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

@@ -324,8 +324,8 @@ 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,

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

@@ -29,9 +29,7 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/watch"
"github.com/eiannone/keyboard"
"github.com/hashicorp/go-multierror"
"github.com/skratchdot/open-golang/open"
)
@@ -71,26 +69,13 @@ func (ke *KeyboardError) error() string {
}
type KeyboardWatch struct {
Watcher watch.Notify
Watching bool
WatchFn func(ctx context.Context, doneCh chan bool, project *types.Project, services []string, options api.WatchOptions) error
Ctx context.Context
Cancel context.CancelFunc
Watcher Toggle
}
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
@@ -110,31 +95,21 @@ type LogKeyboard struct {
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,
},
IsDockerDesktopActive: isDockerDesktopActive,
IsWatchConfigured: true,
logLevel: INFO,
signalChannel: sc,
}
return KeyboardManager
}
func (lk *LogKeyboard) ClearKeyboardInfo() {
@@ -233,48 +208,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,39 +266,34 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
}()
}
func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
if !lk.IsWatchConfigured {
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)
@@ -331,15 +304,16 @@ func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Cont
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 +324,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

13
go.mod
View File

@@ -10,13 +10,14 @@ require (
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/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/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
@@ -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/sys v0.33.0
google.golang.org/grpc v1.72.1
google.golang.org/grpc v1.72.2
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v1.0.1
@@ -86,7 +85,6 @@ require (
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/containerd/api v1.9.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs 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
@@ -162,6 +160,7 @@ require (
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250417144416-3f76f8130144 // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect

18
go.sum
View File

@@ -127,15 +127,15 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
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/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=
@@ -544,8 +544,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=
@@ -625,8 +623,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=

View File

@@ -23,6 +23,7 @@ import (
"fmt"
"net/http"
"path/filepath"
"slices"
"time"
pusherrors "github.com/containerd/containerd/v2/core/remotes/errors"
@@ -157,14 +158,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)
}
}
@@ -535,12 +537,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")),
),
})
}

View File

@@ -22,7 +22,6 @@ import (
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/compose-spec/compose-go/v2/types"
@@ -34,7 +33,6 @@ import (
"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 +69,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 +150,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 +357,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

View File

@@ -51,12 +51,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 +67,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{})
@@ -420,8 +416,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 {
@@ -1253,7 +1254,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, project *types.Proje
}
id, err := s.resolveOrCreateNetwork(ctx, project, name, n)
if errdefs.IsConflict(err) {
if cerrdefs.IsConflict(err) {
// Maybe another execution of `docker compose up|run` created same network
// let's retry once
return s.resolveOrCreateNetwork(ctx, project, name, n)
@@ -1262,6 +1263,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 +1299,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 +1316,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 +1396,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 +1422,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 +1487,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 +1526,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project, assumeYes bool) (string, error) {
inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
if err != nil {
if !errdefs.IsNotFound(err) {
if !cerrdefs.IsNotFound(err) {
return "", err
}
if volume.External {

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)
@@ -196,5 +217,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,12 @@ 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/docker/pkg/stringid"
)
@@ -130,11 +130,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

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,15 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
var isTerminated atomic.Bool
printer := newLogPrinter(options.Start.Attach)
var watcher *Watcher
if options.Start.Watch {
watcher, err = NewWatcher(project, options, s.watch)
if err != nil {
return err
}
}
var navigationMenu *formatter.LogKeyboard
var kEvents <-chan keyboard.KeyEvent
if options.Start.NavigationMenu {
kEvents, err = keyboard.GetKeys(100)
@@ -80,20 +89,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 +115,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 +125,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
}
case <-signalChan:
if first {
keyboard.Close() //nolint:errcheck
gracefulTeardown()
break
}
@@ -129,7 +136,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 +144,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 +164,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,15 @@ 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"))
}

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,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`})
}

View File

@@ -5,7 +5,6 @@
//
// mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/docker/docker/client APIClient
//
// Package mocks is a generated GoMock package.
package mocks

View File

@@ -5,7 +5,6 @@
//
// mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli
//
// Package mocks is a generated GoMock package.
package mocks
@@ -16,12 +15,8 @@ import (
configfile "github.com/docker/cli/cli/config/configfile"
docker "github.com/docker/cli/cli/context/docker"
store "github.com/docker/cli/cli/context/store"
store0 "github.com/docker/cli/cli/manifest/store"
client "github.com/docker/cli/cli/registry/client"
streams "github.com/docker/cli/cli/streams"
trust "github.com/docker/cli/cli/trust"
client0 "github.com/docker/docker/client"
client1 "github.com/theupdateframework/notary/client"
client "github.com/docker/docker/client"
metric "go.opentelemetry.io/otel/metric"
resource "go.opentelemetry.io/otel/sdk/resource"
trace "go.opentelemetry.io/otel/trace"
@@ -85,10 +80,10 @@ func (mr *MockCliMockRecorder) BuildKitEnabled() *gomock.Call {
}
// Client mocks base method.
func (m *MockCli) Client() client0.APIClient {
func (m *MockCli) Client() client.APIClient {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Client")
ret0, _ := ret[0].(client0.APIClient)
ret0, _ := ret[0].(client.APIClient)
return ret0
}
@@ -224,20 +219,6 @@ func (mr *MockCliMockRecorder) In() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "In", reflect.TypeOf((*MockCli)(nil).In))
}
// ManifestStore mocks base method.
func (m *MockCli) ManifestStore() store0.Store {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ManifestStore")
ret0, _ := ret[0].(store0.Store)
return ret0
}
// ManifestStore indicates an expected call of ManifestStore.
func (mr *MockCliMockRecorder) ManifestStore() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ManifestStore", reflect.TypeOf((*MockCli)(nil).ManifestStore))
}
// MeterProvider mocks base method.
func (m *MockCli) MeterProvider() metric.MeterProvider {
m.ctrl.T.Helper()
@@ -252,21 +233,6 @@ func (mr *MockCliMockRecorder) MeterProvider() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MeterProvider", reflect.TypeOf((*MockCli)(nil).MeterProvider))
}
// NotaryClient mocks base method.
func (m *MockCli) NotaryClient(arg0 trust.ImageRefAndAuth, arg1 []string) (client1.Repository, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NotaryClient", arg0, arg1)
ret0, _ := ret[0].(client1.Repository)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NotaryClient indicates an expected call of NotaryClient.
func (mr *MockCliMockRecorder) NotaryClient(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotaryClient", reflect.TypeOf((*MockCli)(nil).NotaryClient), arg0, arg1)
}
// Out mocks base method.
func (m *MockCli) Out() *streams.Out {
m.ctrl.T.Helper()
@@ -281,20 +247,6 @@ func (mr *MockCliMockRecorder) Out() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Out", reflect.TypeOf((*MockCli)(nil).Out))
}
// RegistryClient mocks base method.
func (m *MockCli) RegistryClient(arg0 bool) client.RegistryClient {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RegistryClient", arg0)
ret0, _ := ret[0].(client.RegistryClient)
return ret0
}
// RegistryClient indicates an expected call of RegistryClient.
func (mr *MockCliMockRecorder) RegistryClient(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryClient", reflect.TypeOf((*MockCli)(nil).RegistryClient), arg0)
}
// Resource mocks base method.
func (m *MockCli) Resource() *resource.Resource {
m.ctrl.T.Helper()

View File

@@ -5,7 +5,6 @@
//
// mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service
//
// Package mocks is a generated GoMock package.
package mocks
@@ -199,10 +198,10 @@ func (mr *MockServiceMockRecorder) Generate(ctx, options any) *gomock.Call {
}
// Images mocks base method.
func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) (map[string]api.ImageSummary, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Images", ctx, projectName, options)
ret0, _ := ret[0].([]api.ImageSummary)
ret0, _ := ret[0].(map[string]api.ImageSummary)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -514,17 +513,17 @@ func (mr *MockServiceMockRecorder) Wait(ctx, projectName, options any) *gomock.C
}
// Watch mocks base method.
func (m *MockService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
func (m *MockService) Watch(ctx context.Context, project *types.Project, options api.WatchOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Watch", ctx, project, services, options)
ret := m.ctrl.Call(m, "Watch", ctx, project, options)
ret0, _ := ret[0].(error)
return ret0
}
// Watch indicates an expected call of Watch.
func (mr *MockServiceMockRecorder) Watch(ctx, project, services, options any) *gomock.Call {
func (mr *MockServiceMockRecorder) Watch(ctx, project, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockService)(nil).Watch), ctx, project, services, options)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockService)(nil).Watch), ctx, project, options)
}
// MockLogConsumer is a mock of LogConsumer interface.

View File

@@ -20,12 +20,12 @@ import (
"context"
"fmt"
"io"
"slices"
"strings"
"sync"
"time"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/buger/goterm"
"github.com/docker/go-units"
@@ -77,7 +77,7 @@ func (w *ttyWriter) Event(e Event) {
}
func (w *ttyWriter) event(e Event) {
if !utils.StringContains(w.eventIDs, e.ID) {
if !slices.Contains(w.eventIDs, e.ID) {
w.eventIDs = append(w.eventIDs, e.ID)
}
if _, ok := w.events[e.ID]; ok {

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