Merge branch 'v2' into 8768-avoid-pulling-same-image-multiple-times

This commit is contained in:
Vedant Koditkar 2022-03-05 21:53:55 +05:30
commit 89dfb9140e
32 changed files with 1319 additions and 542 deletions

View File

@ -28,9 +28,9 @@ jobs:
- name: Run golangci-lint
env:
BUILD_TAGS: e2e
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/bin/ v1.39.0
make -f builder.Makefile lint
uses: golangci/golangci-lint-action@v2
with:
args: --timeout=180s
# only on main branch, costs too much for the gain on every PR
validate-cross-build:

View File

@ -120,24 +120,6 @@ func (o *projectOptions) WithServices(fn ProjectServicesFunc) func(cmd *cobra.Co
return err
}
if o.EnvFile != "" {
var services types.Services
for _, s := range project.Services {
ef := o.EnvFile
if ef != "" {
if !filepath.IsAbs(ef) {
ef = filepath.Join(project.WorkingDir, o.EnvFile)
}
if s.Labels == nil {
s.Labels = make(map[string]string)
}
s.Labels[api.EnvironmentFileLabel] = ef
services = append(services, s)
}
}
project.Services = services
}
return fn(ctx, project, args)
})
}
@ -180,6 +162,25 @@ func (o *projectOptions) toProject(services []string, po ...cli.ProjectOptionsFn
compose.Separator = "_"
}
ef := o.EnvFile
if ef != "" && !filepath.IsAbs(ef) {
ef = filepath.Join(project.WorkingDir, o.EnvFile)
}
for i, s := range project.Services {
s.CustomLabels = map[string]string{
api.ProjectLabel: project.Name,
api.ServiceLabel: s.Name,
api.VersionLabel: api.ComposeVersion,
api.WorkingDirLabel: project.WorkingDir,
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
api.OneoffLabel: "False", // default, will be overridden by `run` command
}
if ef != "" {
s.CustomLabels[api.EnvironmentFileLabel] = ef
}
project.Services[i] = s
}
if len(services) > 0 {
s, err := project.GetServices(services...)
if err != nil {

View File

@ -19,10 +19,14 @@ package compose
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/compose-spec/compose-go/types"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/docker/compose/v2/pkg/api"
)
@ -58,10 +62,19 @@ func downCommand(p *projectOptions, backend api.Service) *cobra.Command {
ValidArgsFunction: noCompletion(),
}
flags := downCmd.Flags()
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
removeOrphans := strings.ToLower(os.Getenv("COMPOSE_REMOVE_ORPHANS ")) == "true"
flags.BoolVar(&opts.removeOrphans, "remove-orphans", removeOrphans, "Remove containers for services not defined in the Compose file.")
flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
flags.BoolVarP(&opts.volumes, "volumes", "v", false, " Remove named volumes declared in the `volumes` section of the Compose file and anonymous volumes attached to containers.")
flags.StringVar(&opts.images, "rmi", "", `Remove images used by services. "local" remove only images that don't have a custom tag ("local"|"all")`)
flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
switch name {
case "volume":
name = "volumes"
logrus.Warn("--volume is deprecated, please use --volumes")
}
return pflag.NormalizedName(name)
})
return downCmd
}

View File

@ -92,22 +92,24 @@ func runList(ctx context.Context, backend api.Service, opts lsOptions) error {
view := viewFromStackList(stackList)
return formatter.Print(view, opts.Format, os.Stdout, func(w io.Writer) {
for _, stack := range view {
_, _ = fmt.Fprintf(w, "%s\t%s\n", stack.Name, stack.Status)
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", stack.Name, stack.Status, stack.ConfigFiles)
}
}, "NAME", "STATUS")
}, "NAME", "STATUS", "CONFIG FILES")
}
type stackView struct {
Name string
Status string
Name string
Status string
ConfigFiles string
}
func viewFromStackList(stackList []api.Stack) []stackView {
retList := make([]stackView, len(stackList))
for i, s := range stackList {
retList[i] = stackView{
Name: s.Name,
Status: strings.TrimSpace(fmt.Sprintf("%s %s", s.Status, s.Reason)),
Name: s.Name,
Status: strings.TrimSpace(fmt.Sprintf("%s %s", s.Status, s.Reason)),
ConfigFiles: s.ConfigFiles,
}
}
return retList

View File

@ -65,7 +65,7 @@ func runRemove(ctx context.Context, backend api.Service, opts removeOptions, ser
}
if opts.stop {
err := backend.Stop(ctx, project, api.StopOptions{
err := backend.Stop(ctx, project.Name, api.StopOptions{
Services: services,
})
if err != nil {

View File

@ -49,13 +49,13 @@ func restartCommand(p *projectOptions, backend api.Service) *cobra.Command {
}
func runRestart(ctx context.Context, backend api.Service, opts restartOptions, services []string) error {
project, err := opts.toProject(services)
projectName, err := opts.toProjectName()
if err != nil {
return err
}
timeout := time.Duration(opts.timeout) * time.Second
return backend.Restart(ctx, project, api.RestartOptions{
return backend.Restart(ctx, projectName, api.RestartOptions{
Timeout: &timeout,
Services: services,
})

View File

@ -240,5 +240,5 @@ func startDependencies(ctx context.Context, backend api.Service, project types.P
if err := backend.Create(ctx, &project, api.CreateOptions{}); err != nil {
return err
}
return backend.Start(ctx, &project, api.StartOptions{})
return backend.Start(ctx, project.Name, api.StartOptions{})
}

View File

@ -43,10 +43,12 @@ func startCommand(p *projectOptions, backend api.Service) *cobra.Command {
}
func runStart(ctx context.Context, backend api.Service, opts startOptions, services []string) error {
project, err := opts.toProject(services)
projectName, err := opts.toProjectName()
if err != nil {
return err
}
return backend.Start(ctx, project, api.StartOptions{})
return backend.Start(ctx, projectName, api.StartOptions{
AttachTo: services,
})
}

View File

@ -53,7 +53,7 @@ func stopCommand(p *projectOptions, backend api.Service) *cobra.Command {
}
func runStop(ctx context.Context, backend api.Service, opts stopOptions, services []string) error {
project, err := opts.toProject(services)
projectName, err := opts.toProjectName()
if err != nil {
return err
}
@ -63,7 +63,7 @@ func runStop(ctx context.Context, backend api.Service, opts stopOptions, service
timeoutValue := time.Duration(opts.timeout) * time.Second
timeout = &timeoutValue
}
return backend.Stop(ctx, project, api.StopOptions{
return backend.Stop(ctx, projectName, api.StopOptions{
Timeout: timeout,
Services: services,
})

View File

@ -32,7 +32,16 @@ func generateCliYaml(opts *options) error {
disableFlagsInUseLine(cmd)
cmd.DisableAutoGenTag = true
return clidocstool.GenYamlTree(cmd, opts.target)
tool, err := clidocstool.New(clidocstool.Options{
Root: cmd,
SourceDir: opts.source,
TargetDir: opts.target,
Plugin: true,
})
if err != nil {
return err
}
return tool.GenYamlTree(cmd)
}
func disableFlagsInUseLine(cmd *cobra.Command) {

97
go.mod
View File

@ -6,13 +6,13 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.2
github.com/buger/goterm v1.0.4
github.com/cnabio/cnab-to-oci v0.3.1-beta1
github.com/compose-spec/compose-go v1.0.9
github.com/compose-spec/compose-go v1.1.0
github.com/containerd/console v1.0.3
github.com/containerd/containerd v1.5.8
github.com/containerd/containerd v1.6.0
github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e
github.com/docker/buildx v0.5.2-0.20210422185057-908a856079fc
github.com/docker/cli v20.10.7+incompatible
github.com/docker/cli-docs-tool v0.1.1
github.com/docker/buildx v0.7.1
github.com/docker/cli v20.10.12+incompatible
github.com/docker/cli-docs-tool v0.2.1
github.com/docker/docker v20.10.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0
@ -21,7 +21,7 @@ require (
github.com/hashicorp/go-version v1.3.0
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-shellwords v1.0.12
github.com/moby/buildkit v0.8.2-0.20210401015549-df49b648c8bf
github.com/moby/buildkit v0.9.1-0.20211019185819-8778943ac3da
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/morikuni/aec v1.0.0
github.com/opencontainers/go-digest v1.0.0
@ -40,41 +40,38 @@ require (
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Microsoft/go-winio v0.4.17 // indirect
github.com/Microsoft/hcsshim v0.8.23 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cnabio/cnab-go v0.10.0-beta1 // indirect
github.com/containerd/cgroups v1.0.1 // indirect
github.com/containerd/continuity v0.1.0 // indirect
github.com/containerd/continuity v0.2.2 // indirect
github.com/containerd/typeurl v1.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4-0.20210125172408-38bea2ce277a // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/felixge/httpsnoop v1.0.2 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/go-logr/logr v0.4.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/flock v0.8.0 // indirect
github.com/gogo/googleapis v1.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.11.13 // indirect
github.com/kr/pty v1.1.8 // indirect
github.com/klauspost/compress v1.13.5 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
@ -82,48 +79,66 @@ require (
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/mount v0.2.0 // indirect
github.com/moby/sys/mountinfo v0.4.1 // indirect
github.com/moby/sys/symlink v0.1.0 // indirect
github.com/moby/sys/mountinfo v0.5.0 // indirect
github.com/moby/sys/signal v0.6.0 // indirect
github.com/moby/sys/symlink v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/runc v1.0.2 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/opencontainers/runc v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.7.1 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.10.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/qri-io/jsonpointer v0.1.0 // indirect
github.com/qri-io/jsonschema v0.1.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/theupdateframework/notary v0.6.1 // indirect
github.com/tonistiigi/fsutil v0.0.0-20201103201449-0834f99b7b85 // indirect
github.com/tonistiigi/fsutil v0.0.0-20210818161904-4442383b5028 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/contrib v0.21.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.21.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.21.0 // indirect
go.opentelemetry.io/otel v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 // indirect
go.opentelemetry.io/otel/internal/metric v0.21.0 // indirect
go.opentelemetry.io/otel/metric v0.21.0 // indirect
go.opentelemetry.io/otel/sdk v1.3.0 // indirect
go.opentelemetry.io/otel/trace v1.3.0 // indirect
go.opentelemetry.io/proto/otlp v0.11.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.42.0 // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apimachinery v0.21.0 // indirect
k8s.io/client-go v0.21.0 // indirect
k8s.io/klog/v2 v2.8.0 // indirect
k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.0 // indirect
k8s.io/apimachinery v0.22.5 // indirect
k8s.io/client-go v0.22.5 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)
// (for buildx)
replace github.com/jaguilar/vt100 => github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305
replace (
github.com/docker/cli => github.com/docker/cli v20.10.3-0.20210702143511-f782d1355eff+incompatible
github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220121014307-40bb9831756f+incompatible
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.0.0-20210714055410-d010b05b4939
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/net/http/httptrace/otelhttptrace v0.0.0-20210714055410-d010b05b4939
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => github.com/tonistiigi/opentelemetry-go-contrib/instrumentation/net/http/otelhttp v0.0.0-20210714055410-d010b05b4939
)

744
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -37,11 +37,11 @@ type Service interface {
// Create executes the equivalent to a `compose create`
Create(ctx context.Context, project *types.Project, opts CreateOptions) error
// Start executes the equivalent to a `compose start`
Start(ctx context.Context, project *types.Project, options StartOptions) error
Start(ctx context.Context, projectName string, options StartOptions) error
// Restart restarts containers
Restart(ctx context.Context, project *types.Project, options RestartOptions) error
Restart(ctx context.Context, projectName string, options RestartOptions) error
// Stop executes the equivalent to a `compose stop`
Stop(ctx context.Context, project *types.Project, options StopOptions) error
Stop(ctx context.Context, projectName string, options StopOptions) error
// Up executes the equivalent to a `compose up`
Up(ctx context.Context, project *types.Project, options UpOptions) error
// Down executes the equivalent to a `compose down`
@ -404,10 +404,11 @@ const (
// Stack holds the name and state of a compose application/stack
type Stack struct {
ID string
Name string
Status string
Reason string
ID string
Name string
Status string
ConfigFiles string
Reason string
}
// LogConsumer is a callback to process log messages from services

View File

@ -28,9 +28,9 @@ type ServiceProxy struct {
PushFn func(ctx context.Context, project *types.Project, options PushOptions) error
PullFn func(ctx context.Context, project *types.Project, opts PullOptions) error
CreateFn func(ctx context.Context, project *types.Project, opts CreateOptions) error
StartFn func(ctx context.Context, project *types.Project, options StartOptions) error
RestartFn func(ctx context.Context, project *types.Project, options RestartOptions) error
StopFn func(ctx context.Context, project *types.Project, options StopOptions) error
StartFn func(ctx context.Context, projectName string, options StartOptions) error
RestartFn func(ctx context.Context, projectName string, options RestartOptions) error
StopFn func(ctx context.Context, projectName string, options StopOptions) error
UpFn func(ctx context.Context, project *types.Project, options UpOptions) error
DownFn func(ctx context.Context, projectName string, options DownOptions) error
LogsFn func(ctx context.Context, projectName string, consumer LogConsumer, options LogOptions) error
@ -141,36 +141,27 @@ func (s *ServiceProxy) Create(ctx context.Context, project *types.Project, optio
}
// Start implements Service interface
func (s *ServiceProxy) Start(ctx context.Context, project *types.Project, options StartOptions) error {
func (s *ServiceProxy) Start(ctx context.Context, projectName string, options StartOptions) error {
if s.StartFn == nil {
return ErrNotImplemented
}
for _, i := range s.interceptors {
i(ctx, project)
}
return s.StartFn(ctx, project, options)
return s.StartFn(ctx, projectName, options)
}
// Restart implements Service interface
func (s *ServiceProxy) Restart(ctx context.Context, project *types.Project, options RestartOptions) error {
func (s *ServiceProxy) Restart(ctx context.Context, projectName string, options RestartOptions) error {
if s.RestartFn == nil {
return ErrNotImplemented
}
for _, i := range s.interceptors {
i(ctx, project)
}
return s.RestartFn(ctx, project, options)
return s.RestartFn(ctx, projectName, options)
}
// Stop implements Service interface
func (s *ServiceProxy) Stop(ctx context.Context, project *types.Project, options StopOptions) error {
func (s *ServiceProxy) Stop(ctx context.Context, projectName string, options StopOptions) error {
if s.StopFn == nil {
return ErrNotImplemented
}
for _, i := range s.interceptors {
i(ctx, project)
}
return s.StopFn(ctx, project, options)
return s.StopFn(ctx, projectName, options)
}
// Up implements Service interface

View File

@ -141,7 +141,7 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
if project.Services[i].Labels == nil {
project.Services[i].Labels = types.Labels{}
}
project.Services[i].Labels[api.ImageDigestLabel] = digest
project.Services[i].CustomLabels[api.ImageDigestLabel] = digest
project.Services[i].Image = image
}
}

View File

@ -19,6 +19,7 @@ package compose
import (
"context"
"os"
"path/filepath"
"github.com/compose-spec/compose-go/types"
"github.com/docker/buildx/build"
@ -28,7 +29,7 @@ import (
func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
const drivername = "default"
d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, "", nil, nil, project.WorkingDir)
d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, nil, nil, nil, project.WorkingDir)
if err != nil {
return nil, err
}
@ -47,7 +48,7 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro
w := xprogress.NewPrinter(progressCtx, os.Stdout, mode)
// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
response, err := build.Build(ctx, driverInfo, opts, nil, nil, w)
response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile.Filename), w)
errW := w.Wait()
if err == nil {
err = errW

View File

@ -24,6 +24,7 @@ import (
"strings"
"github.com/docker/compose/v2/pkg/api"
"github.com/pkg/errors"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/cli/config/configfile"
@ -92,3 +93,59 @@ func escapeDollarSign(marshal []byte) []byte {
escDollar := []byte{'$', '$'}
return bytes.ReplaceAll(marshal, dollar, escDollar)
}
// projectFromName builds a types.Project based on actual resources with compose labels set
func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) {
project := &types.Project{
Name: projectName,
}
if len(containers) == 0 {
return project, errors.New("no such project: " + projectName)
}
set := map[string]*types.ServiceConfig{}
for _, c := range containers {
serviceLabel := c.Labels[api.ServiceLabel]
_, ok := set[serviceLabel]
if !ok {
set[serviceLabel] = &types.ServiceConfig{
Name: serviceLabel,
Image: c.Image,
Labels: c.Labels,
}
}
set[serviceLabel].Scale++
}
for _, service := range set {
dependencies := service.Labels[api.DependenciesLabel]
if len(dependencies) > 0 {
service.DependsOn = types.DependsOnConfig{}
for _, dc := range strings.Split(dependencies, ",") {
dcArr := strings.Split(dc, ":")
condition := ServiceConditionRunningOrHealthy
dependency := dcArr[0]
// backward compatibility
if len(dcArr) > 1 {
condition = dcArr[1]
}
service.DependsOn[dependency] = types.ServiceDependency{Condition: condition}
}
}
project.Services = append(project.Services, *service)
}
SERVICES:
for _, qs := range services {
for _, es := range project.Services {
if es.Name == qs {
continue SERVICES
}
}
return project, errors.New("no such service: " + qs)
}
err := project.ForServices(services)
if err != nil {
return project, err
}
return project, nil
}

View File

@ -276,9 +276,10 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
eg, _ := errgroup.WithContext(ctx)
w := progress.ContextWriter(ctx)
for dep, config := range dependencies {
if config.Condition == types.ServiceConditionStarted {
// already managed by InDependencyOrder
return nil
if shouldWait, err := shouldWaitForDependency(dep, config, project); err != nil {
return err
} else if !shouldWait {
continue
}
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, false, dep)
@ -334,6 +335,20 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
return eg.Wait()
}
func shouldWaitForDependency(serviceName string, dependencyConfig types.ServiceDependency, project *types.Project) (bool, error) {
if dependencyConfig.Condition == types.ServiceConditionStarted {
// already managed by InDependencyOrder
return false, nil
}
if service, err := project.GetService(serviceName); err != nil {
return false, err
} else if service.Scale == 0 {
// don't wait for the dependency which configured to have 0 containers running
return false, nil
}
return true, nil
}
func nextContainerNumber(containers []moby.Container) (int, error) {
max := 0
for _, c := range containers {

View File

@ -19,6 +19,7 @@ package compose
import (
"context"
"fmt"
"strings"
"testing"
"github.com/compose-spec/compose-go/types"
@ -184,3 +185,31 @@ func TestServiceLinks(t *testing.T) {
assert.Equal(t, links[2], "testProject-web-1:testProject-web-1")
})
}
func TestWaitDependencies(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api
t.Run("should skip dependencies with scale 0", func(t *testing.T) {
dbService := types.ServiceConfig{Name: "db", Scale: 0}
redisService := types.ServiceConfig{Name: "redis", Scale: 0}
project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{dbService, redisService}}
dependencies := types.DependsOnConfig{
"db": {Condition: ServiceConditionRunningOrHealthy},
"redis": {Condition: ServiceConditionRunningOrHealthy},
}
assert.NilError(t, tested.waitDependencies(context.Background(), &project, dependencies))
})
t.Run("should skip dependencies with condition service_started", func(t *testing.T) {
dbService := types.ServiceConfig{Name: "db", Scale: 1}
redisService := types.ServiceConfig{Name: "redis", Scale: 1}
project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{dbService, redisService}}
dependencies := types.DependsOnConfig{
"db": {Condition: types.ServiceConditionStarted},
"redis": {Condition: types.ServiceConditionStarted},
}
assert.NilError(t, tested.waitDependencies(context.Background(), &project, dependencies))
})
}

View File

@ -229,7 +229,7 @@ func getImageName(service types.ServiceConfig, projectName string) string {
func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig,
number int, inherit *moby.Container, autoRemove bool, attachStdin bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
labels, err := s.prepareLabels(p, service, number)
labels, err := s.prepareLabels(service, number)
if err != nil {
return nil, nil, nil, err
}
@ -414,31 +414,26 @@ func parseSecurityOpts(p *types.Project, securityOpts []string) ([]string, error
return securityOpts, nil
}
func (s *composeService) prepareLabels(p *types.Project, service types.ServiceConfig, number int) (map[string]string, error) {
func (s *composeService) prepareLabels(service types.ServiceConfig, number int) (map[string]string, error) {
labels := map[string]string{}
for k, v := range service.Labels {
labels[k] = v
}
labels[api.ProjectLabel] = p.Name
labels[api.ServiceLabel] = service.Name
labels[api.VersionLabel] = api.ComposeVersion
if _, ok := service.Labels[api.OneoffLabel]; !ok {
labels[api.OneoffLabel] = "False"
for k, v := range service.CustomLabels {
labels[k] = v
}
hash, err := ServiceHash(service)
if err != nil {
return nil, err
}
labels[api.ConfigHashLabel] = hash
labels[api.WorkingDirLabel] = p.WorkingDir
labels[api.ConfigFilesLabel] = strings.Join(p.ComposeFiles, ",")
labels[api.ContainerNumberLabel] = strconv.Itoa(number)
var dependencies []string
for s := range service.DependsOn {
dependencies = append(dependencies, s)
for s, d := range service.DependsOn {
dependencies = append(dependencies, s+":"+d.Condition)
}
labels[api.DependenciesLabel] = strings.Join(dependencies, ",")
return labels, nil
@ -643,15 +638,11 @@ func buildContainerPortBindingOptions(s types.ServiceConfig) nat.PortMap {
bindings := nat.PortMap{}
for _, port := range s.Ports {
p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
bind := bindings[p]
binding := nat.PortBinding{
HostIP: port.HostIP,
HostIP: port.HostIP,
HostPort: port.Published,
}
if port.Published > 0 {
binding.HostPort = fmt.Sprint(port.Published)
}
bind = append(bind, binding)
bindings[p] = bind
bindings[p] = append(bindings[p], binding)
}
return bindings
}
@ -1107,7 +1098,7 @@ func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeCo
return err
}
if volume.External.External {
return fmt.Errorf("external volume %q not found", volume.External.Name)
return fmt.Errorf("external volume %q not found", volume.Name)
}
err := s.createVolume(ctx, volume)
return err

View File

@ -41,6 +41,7 @@ func (s *composeService) Down(ctx context.Context, projectName string, options a
}
func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error {
builtFromResources := options.Project == nil
w := progress.ContextWriter(ctx)
resourceToRemove := false
@ -50,8 +51,8 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
return err
}
if options.Project == nil {
options.Project, err = s.projectFromLabels(ctx, containers.filter(isNotOneOff), projectName)
if builtFromResources {
options.Project, err = s.getProjectWithVolumes(ctx, containers, projectName)
if err != nil {
return err
}
@ -232,34 +233,9 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
return eg.Wait()
}
// projectFromLabels builds a types.Project based on actual resources with compose labels set
func (s *composeService) projectFromLabels(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
project := &types.Project{
Name: projectName,
}
if len(containers) == 0 {
return project, nil
}
set := map[string]moby.Container{}
for _, c := range containers {
set[c.Labels[api.ServiceLabel]] = c
}
for s, c := range set {
service := types.ServiceConfig{
Name: s,
Image: c.Image,
Labels: c.Labels,
}
dependencies := c.Labels[api.DependenciesLabel]
if len(dependencies) > 0 {
service.DependsOn = types.DependsOnConfig{}
for _, d := range strings.Split(dependencies, ",") {
service.DependsOn[d] = types.ServiceDependency{}
}
}
project.Services = append(project.Services, service)
}
func (s *composeService) getProjectWithVolumes(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
containers = containers.filter(isNotOneOff)
project, _ := s.projectFromName(containers, projectName)
volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
if err != nil {
return nil, err
@ -273,6 +249,5 @@ func (s *composeService) projectFromLabels(ctx context.Context, containers Conta
Labels: vol.Labels,
}
}
return project, nil
}

View File

@ -24,7 +24,6 @@ import (
)
// ServiceHash compute configuration has for a service
// TODO move this to compose-go
func ServiceHash(o types.ServiceConfig) (string, error) {
// remove the Build config when generating the service hash
o.Build = nil

View File

@ -20,8 +20,10 @@ import (
"context"
"fmt"
"sort"
"strings"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -46,15 +48,40 @@ func containersToStacks(containers []moby.Container) ([]api.Stack, error) {
}
var projects []api.Stack
for _, project := range keys {
configFiles, err := combinedConfigFiles(containersByLabel[project])
if err != nil {
return nil, err
}
projects = append(projects, api.Stack{
ID: project,
Name: project,
Status: combinedStatus(containerToState(containersByLabel[project])),
ID: project,
Name: project,
Status: combinedStatus(containerToState(containersByLabel[project])),
ConfigFiles: configFiles,
})
}
return projects, nil
}
func combinedConfigFiles(containers []moby.Container) (string, error) {
configFiles := []string{}
for _, c := range containers {
files, ok := c.Labels[api.ConfigFilesLabel]
if !ok {
return "", fmt.Errorf("No label %q set on container %q of compose project", api.ConfigFilesLabel, c.ID)
}
for _, f := range strings.Split(files, ",") {
if !utils.StringContains(configFiles, f) {
configFiles = append(configFiles, f)
}
}
}
return strings.Join(configFiles, ","), nil
}
func containerToState(containers []moby.Container) []string {
statuses := []string{}
for _, c := range containers {

View File

@ -17,6 +17,7 @@
package compose
import (
"fmt"
"testing"
"github.com/docker/compose/v2/pkg/api"
@ -30,31 +31,33 @@ func TestContainersToStacks(t *testing.T) {
{
ID: "service1",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project1"},
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
},
{
ID: "service2",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project1"},
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
},
{
ID: "service3",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project2"},
Labels: map[string]string{api.ProjectLabel: "project2", api.ConfigFilesLabel: "/home/project2-docker-compose.yaml"},
},
}
stacks, err := containersToStacks(containers)
assert.NilError(t, err)
assert.DeepEqual(t, stacks, []api.Stack{
{
ID: "project1",
Name: "project1",
Status: "running(2)",
ID: "project1",
Name: "project1",
Status: "running(2)",
ConfigFiles: "/home/docker-compose.yaml",
},
{
ID: "project2",
Name: "project2",
Status: "running(1)",
ID: "project2",
Name: "project2",
Status: "running(1)",
ConfigFiles: "/home/project2-docker-compose.yaml",
},
})
}
@ -64,3 +67,56 @@ func TestStacksMixedStatus(t *testing.T) {
assert.Equal(t, combinedStatus([]string{"running", "running", "running"}), "running(3)")
assert.Equal(t, combinedStatus([]string{"running", "exited", "running"}), "exited(1), running(2)")
}
func TestCombinedConfigFiles(t *testing.T) {
containersByLabel := map[string][]moby.Container{
"project1": {
{
ID: "service1",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
},
{
ID: "service2",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project1", api.ConfigFilesLabel: "/home/docker-compose.yaml"},
},
},
"project2": {
{
ID: "service3",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project2", api.ConfigFilesLabel: "/home/project2-docker-compose.yaml"},
},
},
"project3": {
{
ID: "service4",
State: "running",
Labels: map[string]string{api.ProjectLabel: "project3"},
},
},
}
testData := map[string]struct {
ConfigFiles string
Error error
}{
"project1": {ConfigFiles: "/home/docker-compose.yaml", Error: nil},
"project2": {ConfigFiles: "/home/project2-docker-compose.yaml", Error: nil},
"project3": {ConfigFiles: "", Error: fmt.Errorf("No label %q set on container %q of compose project", api.ConfigFilesLabel, "service4")},
}
for project, containers := range containersByLabel {
configFiles, err := combinedConfigFiles(containers)
expected := testData[project]
if expected.Error != nil {
assert.Equal(t, err.Error(), expected.Error.Error())
} else {
assert.Equal(t, err, expected.Error)
}
assert.Equal(t, configFiles, expected.ConfigFiles)
}
}

View File

@ -33,7 +33,7 @@ func (s *composeService) Pause(ctx context.Context, project string, options api.
}
func (s *composeService) pause(ctx context.Context, project string, options api.PauseOptions) error {
containers, err := s.getContainers(ctx, project, oneOffExclude, true, options.Services...)
containers, err := s.getContainers(ctx, project, oneOffExclude, false, options.Services...)
if err != nil {
return err
}
@ -61,7 +61,7 @@ func (s *composeService) UnPause(ctx context.Context, project string, options ap
}
func (s *composeService) unPause(ctx context.Context, project string, options api.PauseOptions) error {
containers, err := s.getContainers(ctx, project, oneOffExclude, true, options.Services...)
containers, err := s.getContainers(ctx, project, oneOffExclude, false, options.Services...)
if err != nil {
return err
}

View File

@ -19,7 +19,6 @@ package compose
import (
"context"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/api"
"golang.org/x/sync/errgroup"
@ -27,14 +26,20 @@ import (
"github.com/docker/compose/v2/pkg/utils"
)
func (s *composeService) Restart(ctx context.Context, project *types.Project, options api.RestartOptions) error {
func (s *composeService) Restart(ctx context.Context, projectName string, options api.RestartOptions) error {
return progress.Run(ctx, func(ctx context.Context) error {
return s.restart(ctx, project, options)
return s.restart(ctx, projectName, options)
})
}
func (s *composeService) restart(ctx context.Context, project *types.Project, options api.RestartOptions) error {
observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
func (s *composeService) restart(ctx context.Context, projectName string, options api.RestartOptions) error {
observedState, err := s.getContainers(ctx, projectName, oneOffInclude, true)
if err != nil {
return err
}
project, err := s.projectFromName(observedState, projectName, options.Services...)
if err != nil {
return err
}

View File

@ -153,8 +153,9 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
if service.Deploy != nil {
service.Deploy.RestartPolicy = nil
}
service.Labels = service.Labels.Add(api.SlugLabel, slug)
service.Labels = service.Labels.Add(api.OneoffLabel, "True")
service.CustomLabels = service.CustomLabels.
Add(api.SlugLabel, slug).
Add(api.OneoffLabel, "True")
if err := s.ensureImagesExists(ctx, project, opts.QuietPull); err != nil { // all dependencies already checked, but might miss service img
return "", err

View File

@ -28,15 +28,22 @@ import (
"github.com/docker/compose/v2/pkg/progress"
)
func (s *composeService) Start(ctx context.Context, project *types.Project, options api.StartOptions) error {
func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
return progress.Run(ctx, func(ctx context.Context) error {
return s.start(ctx, project, options, nil)
return s.start(ctx, projectName, options, nil)
})
}
func (s *composeService) start(ctx context.Context, project *types.Project, options api.StartOptions, listener api.ContainerEventListener) error {
if len(options.AttachTo) == 0 {
options.AttachTo = project.ServiceNames()
func (s *composeService) start(ctx context.Context, projectName string, options api.StartOptions, listener api.ContainerEventListener) error {
var containers Containers
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
if err != nil {
return err
}
project, err := s.projectFromName(containers, projectName, options.AttachTo...)
if err != nil {
return err
}
eg, ctx := errgroup.WithContext(ctx)
@ -53,7 +60,7 @@ func (s *composeService) start(ctx context.Context, project *types.Project, opti
})
}
err := InDependencyOrder(ctx, project, func(c context.Context, name string) error {
err = InDependencyOrder(ctx, project, func(c context.Context, name string) error {
service, err := project.GetService(name)
if err != nil {
return err

View File

@ -21,25 +21,29 @@ import (
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/compose-spec/compose-go/types"
)
func (s *composeService) Stop(ctx context.Context, project *types.Project, options api.StopOptions) error {
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
return progress.Run(ctx, func(ctx context.Context) error {
return s.stop(ctx, project, options)
return s.stop(ctx, projectName, options)
})
}
func (s *composeService) stop(ctx context.Context, project *types.Project, options api.StopOptions) error {
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions) error {
w := progress.ContextWriter(ctx)
services := options.Services
if len(services) == 0 {
services = project.ServiceNames()
services = []string{}
}
var containers Containers
containers, err := s.getContainers(ctx, project.Name, oneOffInclude, true, services...)
containers, err := s.getContainers(ctx, projectName, oneOffInclude, true, services...)
if err != nil {
return err
}
project, err := s.projectFromName(containers, projectName, services...)
if err != nil {
return err
}

View File

@ -25,7 +25,6 @@ import (
compose "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/mocks"
"github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types"
"github.com/golang/mock/gomock"
"gotest.tools/v3/assert"
@ -50,13 +49,7 @@ func TestStopTimeout(t *testing.T) {
api.EXPECT().ContainerStop(gomock.Any(), "456", &timeout).Return(nil)
api.EXPECT().ContainerStop(gomock.Any(), "789", &timeout).Return(nil)
err := tested.Stop(ctx, &types.Project{
Name: strings.ToLower(testProject),
Services: []types.ServiceConfig{
{Name: "service1"},
{Name: "service2"},
},
}, compose.StopOptions{
err := tested.Stop(ctx, strings.ToLower(testProject), compose.StopOptions{
Timeout: &timeout,
})
assert.NilError(t, err)

View File

@ -38,7 +38,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
return err
}
if options.Start.Attach == nil {
return s.start(ctx, project, options.Start, nil)
return s.start(ctx, project.Name, options.Start, nil)
}
return nil
})
@ -65,7 +65,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
})
}()
return s.Stop(ctx, project, api.StopOptions{
return s.Stop(ctx, project.Name, api.StopOptions{
Services: options.Create.Services,
})
})
@ -85,7 +85,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
return err
})
err = s.start(ctx, project, options.Start, printer.HandleEvent)
err = s.start(ctx, project.Name, options.Start, printer.HandleEvent)
if err != nil {
return err
}

File diff suppressed because it is too large Load Diff