mirror of
https://github.com/docker/compose.git
synced 2026-02-10 18:49:28 +08:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2b9c81254 | ||
|
|
d75f22cc7b | ||
|
|
2282159922 | ||
|
|
8c7951465e | ||
|
|
9054f6a18b | ||
|
|
0ef4b90fae | ||
|
|
908c12120e | ||
|
|
dbe7de50a7 | ||
|
|
fcff39631a | ||
|
|
bcaa908f74 | ||
|
|
c64b044b7e | ||
|
|
1a4e6c2465 | ||
|
|
67b4669f9b | ||
|
|
61735c0012 | ||
|
|
c12a948f97 | ||
|
|
bf5785307b | ||
|
|
52eeda9aa7 | ||
|
|
1f0cf0723c | ||
|
|
315f16e5fb | ||
|
|
327a1bb27b | ||
|
|
16914e372e | ||
|
|
ec080f184a | ||
|
|
b5b8e7a116 | ||
|
|
9d73cc88cc | ||
|
|
9c68c76bea | ||
|
|
aeb7448449 | ||
|
|
42c3adb236 | ||
|
|
35f37cd1f7 | ||
|
|
c0465616bb | ||
|
|
32d44dfc25 | ||
|
|
5885a250bc | ||
|
|
ce1c788237 | ||
|
|
67f7b84829 | ||
|
|
fd676adc5d | ||
|
|
aa864fde20 | ||
|
|
f7a6c3bc54 | ||
|
|
8a9498c571 | ||
|
|
7e7262bc77 | ||
|
|
981aea674d | ||
|
|
64a9e4bf01 | ||
|
|
4be38f84df | ||
|
|
09e0fa94b8 | ||
|
|
416498441c | ||
|
|
cb45c6f2df | ||
|
|
90ca37344f | ||
|
|
fb3f9e270f | ||
|
|
02f78d2893 | ||
|
|
d47dcef1a6 | ||
|
|
fb9310caf2 | ||
|
|
ced9eba940 | ||
|
|
2eeed8481d | ||
|
|
10ca0314bc | ||
|
|
336b825fdd | ||
|
|
213d9166dc | ||
|
|
598b59f8bf | ||
|
|
65ed8cf4c2 | ||
|
|
a23cbb580e | ||
|
|
a89e194558 | ||
|
|
b31695a66e | ||
|
|
5262d3bbf5 | ||
|
|
a4836391a5 | ||
|
|
bfd7428619 | ||
|
|
feba34e406 | ||
|
|
37f763f009 | ||
|
|
99cd90a4b2 | ||
|
|
a279c3a934 | ||
|
|
ee586e7f1e | ||
|
|
5eb314a4ca | ||
|
|
c5cdce0b60 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -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:
|
||||
|
||||
3
.github/workflows/release.yaml
vendored
3
.github/workflows/release.yaml
vendored
@@ -44,7 +44,8 @@ jobs:
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "bin/*"
|
||||
prerelease: true
|
||||
generateReleaseNotes: true
|
||||
draft: true
|
||||
commit: "v2"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.event.inputs.tag }}
|
||||
|
||||
@@ -83,7 +83,7 @@ don't get discouraged! Our contributor's guide explains
|
||||
<tr>
|
||||
<td>Community Slack</td>
|
||||
<td>
|
||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockercommunity.slack.com/ssb/redirect" target="_blank">with this link</a>.
|
||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://www.docker.com/docker-community" target="_blank">with this link</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -120,24 +121,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)
|
||||
})
|
||||
}
|
||||
@@ -176,10 +159,29 @@ func (o *projectOptions) toProject(services []string, po ...cli.ProjectOptionsFn
|
||||
return nil, compose.WrapComposeError(err)
|
||||
}
|
||||
|
||||
if o.Compatibility || project.Environment["COMPOSE_COMPATIBILITY"] == "true" {
|
||||
if o.Compatibility || utils.StringToBool(project.Environment["COMPOSE_COMPATIBILITY"]) {
|
||||
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 {
|
||||
|
||||
@@ -116,7 +116,9 @@ func runConvert(ctx context.Context, backend api.Service, opts convertOptions, s
|
||||
project, err := opts.toProject(services,
|
||||
cli.WithInterpolation(!opts.noInterpolate),
|
||||
cli.WithResolvedPaths(true),
|
||||
cli.WithNormalization(!opts.noNormalize))
|
||||
cli.WithNormalization(!opts.noNormalize),
|
||||
cli.WithDiscardEnvFile)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,10 +19,14 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"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 := utils.StringToBool(os.Getenv("COMPOSE_REMOVE_ORPHANS "))
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@ func execCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", false, "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.")
|
||||
runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command.")
|
||||
|
||||
runCmd.Flags().BoolP("interactive", "i", true, "Keep STDIN open even if not attached. DEPRECATED")
|
||||
runCmd.Flags().MarkHidden("interactive") //nolint:errcheck
|
||||
runCmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY. DEPRECATED")
|
||||
runCmd.Flags().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
runCmd.Flags().SetInterspersed(false)
|
||||
return runCmd
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -42,6 +42,7 @@ type runOptions struct {
|
||||
Detach bool
|
||||
Remove bool
|
||||
noTty bool
|
||||
interactive bool
|
||||
user string
|
||||
workdir string
|
||||
entrypoint string
|
||||
@@ -53,6 +54,7 @@ type runOptions struct {
|
||||
servicePorts bool
|
||||
name string
|
||||
noDeps bool
|
||||
ignoreOrphans bool
|
||||
quietPull bool
|
||||
}
|
||||
|
||||
@@ -134,6 +136,8 @@ func runCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ignore := project.Environment["COMPOSE_IGNORE_ORPHANS"]
|
||||
opts.ignoreOrphans = strings.ToLower(ignore) == "true"
|
||||
return runRun(ctx, backend, project, opts)
|
||||
}),
|
||||
ValidArgsFunction: serviceCompletion(p),
|
||||
@@ -155,6 +159,10 @@ func runCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
flags.BoolVar(&opts.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.")
|
||||
flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information.")
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.")
|
||||
cmd.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY.")
|
||||
cmd.Flags().MarkHidden("tty") //nolint:errcheck
|
||||
|
||||
flags.SetNormalizeFunc(normalizeRunFlags)
|
||||
flags.SetInterspersed(false)
|
||||
return cmd
|
||||
@@ -177,7 +185,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
}
|
||||
|
||||
err = progress.Run(ctx, func(ctx context.Context) error {
|
||||
return startDependencies(ctx, backend, *project, opts.Service)
|
||||
return startDependencies(ctx, backend, *project, opts.Service, opts.ignoreOrphans)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -213,6 +221,14 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
Index: 0,
|
||||
QuietPull: opts.quietPull,
|
||||
}
|
||||
|
||||
for i, service := range project.Services {
|
||||
if service.Name == opts.Service {
|
||||
service.StdinOpen = opts.interactive
|
||||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
|
||||
exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
|
||||
if exitCode != 0 {
|
||||
errMsg := ""
|
||||
@@ -224,7 +240,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||
return err
|
||||
}
|
||||
|
||||
func startDependencies(ctx context.Context, backend api.Service, project types.Project, requestedServiceName string) error {
|
||||
func startDependencies(ctx context.Context, backend api.Service, project types.Project, requestedServiceName string, ignoreOrphans bool) error {
|
||||
dependencies := types.Services{}
|
||||
var requestedService types.ServiceConfig
|
||||
for _, service := range project.Services {
|
||||
@@ -237,8 +253,15 @@ func startDependencies(ctx context.Context, backend api.Service, project types.P
|
||||
|
||||
project.Services = dependencies
|
||||
project.DisabledServices = append(project.DisabledServices, requestedService)
|
||||
if err := backend.Create(ctx, &project, api.CreateOptions{}); err != nil {
|
||||
err := backend.Create(ctx, &project, api.CreateOptions{
|
||||
IgnoreOrphans: ignoreOrphans,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return backend.Start(ctx, &project, api.StartOptions{})
|
||||
|
||||
if len(dependencies) > 0 {
|
||||
return backend.Start(ctx, project.Name, api.StartOptions{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -103,8 +103,7 @@ func upCommand(p *projectOptions, backend api.Service) *cobra.Command {
|
||||
return validateFlags(&up, &create)
|
||||
}),
|
||||
RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
|
||||
ignore := project.Environment["COMPOSE_IGNORE_ORPHANS"]
|
||||
create.ignoreOrphans = strings.ToLower(ignore) == "true"
|
||||
create.ignoreOrphans = utils.StringToBool(project.Environment["COMPOSE_IGNORE_ORPHANS"])
|
||||
if create.ignoreOrphans && create.removeOrphans {
|
||||
return fmt.Errorf("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined")
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
|
||||
@@ -52,11 +53,11 @@ func versionCommand() *cobra.Command {
|
||||
|
||||
func runVersion(opts versionOptions) {
|
||||
if opts.short {
|
||||
fmt.Println(internal.Version)
|
||||
fmt.Println(strings.TrimPrefix(internal.Version, "v"))
|
||||
return
|
||||
}
|
||||
if opts.format == formatter.JSON {
|
||||
fmt.Printf(`{"version":%q}\n`, internal.Version)
|
||||
fmt.Printf("{\"version\":%q}\n", internal.Version)
|
||||
return
|
||||
}
|
||||
fmt.Println("Docker Compose version", internal.Version)
|
||||
|
||||
@@ -99,3 +99,6 @@ Setting the `COMPOSE_FILE` environment variable is equivalent to passing the `-f
|
||||
and so does `COMPOSE_PROFILES` environment variable for to the `--profiles` flag.
|
||||
|
||||
If flags are explicitly set on command line, associated environment variable is ignored
|
||||
|
||||
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` will stop docker compose from detecting orphaned
|
||||
containers for the project.
|
||||
|
||||
@@ -5,7 +5,7 @@ Builds, (re)creates, starts, and attaches to containers for a service.
|
||||
|
||||
Unless they are already running, this command also starts any linked services.
|
||||
|
||||
The `docker compose up` command aggregates the output of each container (liked `docker compose logs --follow` does).
|
||||
The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does).
|
||||
When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the
|
||||
background and leaves them running.
|
||||
|
||||
|
||||
@@ -98,6 +98,9 @@ long: |-
|
||||
and so does `COMPOSE_PROFILES` environment variable for to the `--profiles` flag.
|
||||
|
||||
If flags are explicitly set on command line, associated environment variable is ignored
|
||||
|
||||
Setting the `COMPOSE_IGNORE_ORPHANS` environment variable to `true` will stop docker compose from detecting orphaned
|
||||
containers for the project.
|
||||
usage: docker compose
|
||||
pname: docker
|
||||
plink: docker.yaml
|
||||
|
||||
@@ -5,7 +5,7 @@ long: |-
|
||||
|
||||
Unless they are already running, this command also starts any linked services.
|
||||
|
||||
The `docker compose up` command aggregates the output of each container (liked `docker compose logs --follow` does).
|
||||
The `docker compose up` command aggregates the output of each container (like `docker compose logs --follow` does).
|
||||
When the command exits, all containers are stopped. Running `docker compose up --detach` starts the containers in the
|
||||
background and leaves them running.
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
102
go.mod
102
go.mod
@@ -4,15 +4,15 @@ go 1.17
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2
|
||||
github.com/buger/goterm v1.0.1
|
||||
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-0.20220101154228-91ed80f52afe
|
||||
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.1
|
||||
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
|
||||
@@ -34,48 +34,44 @@ require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gotest.tools v2.2.0+incompatible
|
||||
gotest.tools/v3 v3.0.3
|
||||
gotest.tools/v3 v3.1.0
|
||||
)
|
||||
|
||||
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/compose-spec/godotenv v1.1.1 // 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
|
||||
@@ -83,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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.Wrap(api.ErrNotFound, fmt.Sprintf("no container found for project %q", 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
|
||||
}
|
||||
|
||||
@@ -261,12 +261,33 @@ func getContainerProgressName(container moby.Container) string {
|
||||
return "Container " + getCanonicalContainerName(container)
|
||||
}
|
||||
|
||||
func containerEvents(containers Containers, eventFunc func(string) progress.Event) []progress.Event {
|
||||
events := []progress.Event{}
|
||||
for _, container := range containers {
|
||||
events = append(events, eventFunc(getContainerProgressName(container)))
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// ServiceConditionRunningOrHealthy is a service condition on statys running or healthy
|
||||
const ServiceConditionRunningOrHealthy = "running_or_healthy"
|
||||
|
||||
func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, dependencies types.DependsOnConfig) error {
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
w := progress.ContextWriter(ctx)
|
||||
for dep, config := range dependencies {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Events(containerEvents(containers, progress.Waiting))
|
||||
|
||||
dep, config := dep, config
|
||||
eg.Go(func() error {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
@@ -280,6 +301,7 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
return err
|
||||
}
|
||||
if healthy {
|
||||
w.Events(containerEvents(containers, progress.Healthy))
|
||||
return nil
|
||||
}
|
||||
case types.ServiceConditionHealthy:
|
||||
@@ -288,6 +310,7 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
return err
|
||||
}
|
||||
if healthy {
|
||||
w.Events(containerEvents(containers, progress.Healthy))
|
||||
return nil
|
||||
}
|
||||
case types.ServiceConditionCompletedSuccessfully:
|
||||
@@ -296,14 +319,12 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
return err
|
||||
}
|
||||
if exited {
|
||||
w.Events(containerEvents(containers, progress.Exited))
|
||||
if code != 0 {
|
||||
return fmt.Errorf("service %q didn't completed successfully: exit %d", dep, code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case types.ServiceConditionStarted:
|
||||
// already managed by InDependencyOrder
|
||||
return nil
|
||||
default:
|
||||
logrus.Warnf("unsupported depends_on condition: %s", config.Condition)
|
||||
return nil
|
||||
@@ -314,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 {
|
||||
@@ -596,8 +631,15 @@ func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Pr
|
||||
if container.State == nil || container.State.Health == nil {
|
||||
return false, fmt.Errorf("container for service %q has no healthcheck configured", service)
|
||||
}
|
||||
if container.State.Health.Status != moby.Healthy {
|
||||
switch container.State.Health.Status {
|
||||
case moby.Healthy:
|
||||
// Continue by checking the next container.
|
||||
case moby.Unhealthy:
|
||||
return false, fmt.Errorf("container for service %q is unhealthy", service)
|
||||
case moby.Starting:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("container for service %q had unexpected health status %q", service, container.State.Health.Status)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,45 +414,42 @@ 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
|
||||
}
|
||||
|
||||
func getDefaultNetworkMode(project *types.Project, service types.ServiceConfig) string {
|
||||
mode := "none"
|
||||
if len(project.Networks) > 0 {
|
||||
for name := range getNetworksForService(service) {
|
||||
mode = project.Networks[name].Name
|
||||
break
|
||||
}
|
||||
if len(project.Networks) == 0 {
|
||||
return "none"
|
||||
}
|
||||
return mode
|
||||
|
||||
if len(service.Networks) > 0 {
|
||||
name := service.NetworksByPriority()[0]
|
||||
return project.Networks[name].Name
|
||||
}
|
||||
|
||||
return project.Networks["default"].Name
|
||||
}
|
||||
|
||||
func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
|
||||
@@ -641,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
|
||||
}
|
||||
@@ -1013,16 +1006,6 @@ func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
|
||||
return aliases
|
||||
}
|
||||
|
||||
func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
|
||||
if len(s.Networks) > 0 {
|
||||
return s.Networks
|
||||
}
|
||||
if s.NetworkMode != "" {
|
||||
return nil
|
||||
}
|
||||
return map[string]*types.ServiceNetworkConfig{"default": nil}
|
||||
}
|
||||
|
||||
func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
|
||||
_, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
|
||||
if err != nil {
|
||||
@@ -1115,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
|
||||
|
||||
@@ -151,3 +151,74 @@ func TestGetBindMode(t *testing.T) {
|
||||
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxShared}, true), "ro,z")
|
||||
assert.Equal(t, getBindMode(&composetypes.ServiceVolumeBind{SELinux: composetypes.SELinuxPrivate}, true), "ro,Z")
|
||||
}
|
||||
|
||||
func TestGetDefaultNetworkMode(t *testing.T) {
|
||||
t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) {
|
||||
service := composetypes.ServiceConfig{
|
||||
Name: "myService",
|
||||
Networks: map[string]*composetypes.ServiceNetworkConfig{
|
||||
"myNetwork1": {
|
||||
Priority: 10,
|
||||
},
|
||||
"myNetwork2": {
|
||||
Priority: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
service,
|
||||
},
|
||||
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
|
||||
"myNetwork1": {
|
||||
Name: "myProject_myNetwork1",
|
||||
},
|
||||
"myNetwork2": {
|
||||
Name: "myProject_myNetwork2",
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_myNetwork2")
|
||||
})
|
||||
|
||||
t.Run("returns default network when service has no networks", func(t *testing.T) {
|
||||
service := composetypes.ServiceConfig{
|
||||
Name: "myService",
|
||||
}
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
service,
|
||||
},
|
||||
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
|
||||
"myNetwork1": {
|
||||
Name: "myProject_myNetwork1",
|
||||
},
|
||||
"myNetwork2": {
|
||||
Name: "myProject_myNetwork2",
|
||||
},
|
||||
"default": {
|
||||
Name: "myProject_default",
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_default")
|
||||
})
|
||||
|
||||
t.Run("returns none if project has no networks", func(t *testing.T) {
|
||||
service := composetypes.ServiceConfig{
|
||||
Name: "myService",
|
||||
}
|
||||
project := composetypes.Project{
|
||||
Name: "myProject",
|
||||
Services: []composetypes.ServiceConfig{
|
||||
service,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, getDefaultNetworkMode(&project, service), "none")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,11 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
return err
|
||||
}
|
||||
|
||||
if options.Project == nil {
|
||||
options.Project = s.projectFromContainerLabels(containers.filter(isNotOneOff), projectName)
|
||||
if builtFromResources {
|
||||
options.Project, err = s.getProjectWithVolumes(ctx, containers, projectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(containers) > 0 {
|
||||
@@ -85,11 +89,7 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
}
|
||||
|
||||
if options.Volumes {
|
||||
rm, err := s.ensureVolumesDown(ctx, projectName, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ops = append(ops, rm...)
|
||||
ops = append(ops, s.ensureVolumesDown(ctx, options.Project, w)...)
|
||||
}
|
||||
|
||||
if !resourceToRemove && len(ops) == 0 {
|
||||
@@ -103,19 +103,15 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) ensureVolumesDown(ctx context.Context, projectName string, w progress.Writer) ([]downOp, error) {
|
||||
func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
|
||||
var ops []downOp
|
||||
volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
|
||||
if err != nil {
|
||||
return ops, err
|
||||
}
|
||||
for _, vol := range volumes.Volumes {
|
||||
id := vol.Name
|
||||
for _, vol := range project.Volumes {
|
||||
volumeName := vol.Name
|
||||
ops = append(ops, func() error {
|
||||
return s.removeVolume(ctx, id, w)
|
||||
return s.removeVolume(ctx, volumeName, w)
|
||||
})
|
||||
}
|
||||
return ops, nil
|
||||
return ops
|
||||
}
|
||||
|
||||
func (s *composeService) ensureImagesDown(ctx context.Context, projectName string, options api.DownOptions, w progress.Writer) []downOp {
|
||||
@@ -237,31 +233,21 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) projectFromContainerLabels(containers Containers, projectName string) *types.Project {
|
||||
project := &types.Project{
|
||||
Name: projectName,
|
||||
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
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return project
|
||||
}
|
||||
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,
|
||||
|
||||
project.Volumes = types.Volumes{}
|
||||
for _, vol := range volumes.Volumes {
|
||||
project.Volumes[vol.Labels[api.VolumeLabel]] = types.VolumeConfig{
|
||||
Name: vol.Name,
|
||||
Driver: vol.Driver,
|
||||
Labels: vol.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)
|
||||
}
|
||||
return project
|
||||
return project, nil
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ func TestDown(t *testing.T) {
|
||||
testContainer("service2", "789", false),
|
||||
testContainer("service_orphan", "321", true),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
Return(volume.VolumeListOKBody{}, nil)
|
||||
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "456", nil).Return(nil)
|
||||
@@ -74,6 +76,8 @@ func TestDownRemoveOrphans(t *testing.T) {
|
||||
testContainer("service2", "789", false),
|
||||
testContainer("service_orphan", "321", true),
|
||||
}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
Return(volume.VolumeListOKBody{}, nil)
|
||||
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "789", nil).Return(nil)
|
||||
@@ -100,13 +104,16 @@ func TestDownRemoveVolumes(t *testing.T) {
|
||||
|
||||
api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(
|
||||
[]moby.Container{testContainer("service1", "123", false)}, nil)
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).
|
||||
Return(volume.VolumeListOKBody{
|
||||
Volumes: []*moby.Volume{{Name: "myProject_volume"}},
|
||||
}, nil)
|
||||
|
||||
api.EXPECT().ContainerStop(gomock.Any(), "123", nil).Return(nil)
|
||||
api.EXPECT().ContainerRemove(gomock.Any(), "123", moby.ContainerRemoveOptions{Force: true, RemoveVolumes: true}).Return(nil)
|
||||
|
||||
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(strings.ToLower(testProject)))}).Return(nil, nil)
|
||||
|
||||
api.EXPECT().VolumeList(gomock.Any(), filters.NewArgs(projectFilter(strings.ToLower(testProject)))).Return(volume.VolumeListOKBody{Volumes: []*moby.Volume{{Name: "myProject_volume"}}}, nil)
|
||||
api.EXPECT().VolumeRemove(gomock.Any(), "myProject_volume", true).Return(nil)
|
||||
|
||||
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{Volumes: true})
|
||||
|
||||
@@ -96,7 +96,7 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption
|
||||
}
|
||||
|
||||
in := streams.NewIn(opts.Stdin)
|
||||
if in.IsTerminal() {
|
||||
if in.IsTerminal() && opts.Tty {
|
||||
state, err := term.SetRawTerminal(in.FD())
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,29 +21,37 @@ 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 && !api.IsNotFoundError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) > 0 {
|
||||
containers = containers.filter(isService(services...))
|
||||
}
|
||||
|
||||
return InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
|
||||
return s.stopContainers(ctx, w, containers.filter(isService(service)), options.Timeout)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -98,8 +98,22 @@ func TestLocalComposeRun(t *testing.T) {
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "8081->80/tcp"), res.Stdout())
|
||||
})
|
||||
|
||||
t.Run("compose run orphan", func(t *testing.T) {
|
||||
// Use different compose files to get an orphan container
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/run-test/orphan.yaml", "run", "simple")
|
||||
res := c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello")
|
||||
assert.Assert(t, strings.Contains(res.Combined(), "orphan"))
|
||||
|
||||
cmd := c.NewDockerCmd("compose", "-f", "./fixtures/run-test/compose.yaml", "run", "back", "echo", "Hello")
|
||||
res = icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
|
||||
cmd.Env = append(cmd.Env, "COMPOSE_IGNORE_ORPHANS=True")
|
||||
})
|
||||
assert.Assert(t, !strings.Contains(res.Combined(), "orphan"))
|
||||
})
|
||||
|
||||
t.Run("down", func(t *testing.T) {
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/run-test/compose.yaml", "down")
|
||||
c.RunDockerComposeCmd("-f", "./fixtures/run-test/orphan.yaml", "down")
|
||||
res := c.RunDockerCmd("ps", "--all")
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "run-test"), res.Stdout())
|
||||
})
|
||||
|
||||
5
pkg/e2e/fixtures/run-test/orphan.yaml
Normal file
5
pkg/e2e/fixtures/run-test/orphan.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
simple:
|
||||
image: alpine
|
||||
command: echo "Hi there!!"
|
||||
14
pkg/e2e/fixtures/start-fail/compose.yaml
Normal file
14
pkg/e2e/fixtures/start-fail/compose.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
fail:
|
||||
image: alpine
|
||||
command: sleep infinity
|
||||
healthcheck:
|
||||
test: "false"
|
||||
interval: 1s
|
||||
retries: 3
|
||||
depends:
|
||||
image: alpine
|
||||
command: sleep infinity
|
||||
depends_on:
|
||||
fail:
|
||||
condition: service_healthy
|
||||
33
pkg/e2e/start_fail_test.go
Normal file
33
pkg/e2e/start_fail_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
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 (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
func TestStartFail(t *testing.T) {
|
||||
c := NewParallelE2eCLI(t, binDir)
|
||||
const projectName = "e2e-start-fail"
|
||||
|
||||
res := c.RunDockerOrExitError("compose", "-f", "fixtures/start-fail/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
res.Assert(t, icmd.Expected{ExitCode: 1, Err: `container for service "fail" is unhealthy`})
|
||||
|
||||
c.RunDockerComposeCmd("--project-name", projectName, "down")
|
||||
}
|
||||
@@ -83,8 +83,8 @@ func TestLocalComposeVolume(t *testing.T) {
|
||||
|
||||
t.Run("cleanup volume project", func(t *testing.T) {
|
||||
c.RunDockerComposeCmd("--project-name", projectName, "down", "--volumes")
|
||||
res := c.RunDockerCmd("volume", "ls")
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), projectName+"_staticVol"))
|
||||
assert.Assert(t, !strings.Contains(res.Stdout(), "myvolume"))
|
||||
ls := c.RunDockerCmd("volume", "ls").Stdout()
|
||||
assert.Assert(t, !strings.Contains(ls, projectName+"_staticVol"))
|
||||
assert.Assert(t, !strings.Contains(ls, "myvolume"))
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,21 @@ func StartedEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Started")
|
||||
}
|
||||
|
||||
// Waiting creates a new waiting event
|
||||
func Waiting(ID string) Event {
|
||||
return NewEvent(ID, Working, "Waiting")
|
||||
}
|
||||
|
||||
// Healthy creates a new healthy event
|
||||
func Healthy(ID string) Event {
|
||||
return NewEvent(ID, Done, "Healthy")
|
||||
}
|
||||
|
||||
// Exited creates a new exited event
|
||||
func Exited(ID string) Event {
|
||||
return NewEvent(ID, Done, "Exited")
|
||||
}
|
||||
|
||||
// RestartingEvent creates a new Restarting in progress Event
|
||||
func RestartingEvent(ID string) Event {
|
||||
return NewEvent(ID, Working, "Restarting")
|
||||
|
||||
@@ -27,7 +27,10 @@ func (p *noopWriter) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *noopWriter) Event(e Event) {
|
||||
func (p *noopWriter) Event(Event) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) Events([]Event) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) TailMsgf(_ string, _ ...interface{}) {
|
||||
|
||||
@@ -40,6 +40,12 @@ func (p *plainWriter) Event(e Event) {
|
||||
fmt.Fprintln(p.out, e.ID, e.Text, e.StatusText)
|
||||
}
|
||||
|
||||
func (p *plainWriter) Events(events []Event) {
|
||||
for _, e := range events {
|
||||
p.Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *plainWriter) TailMsgf(m string, args ...interface{}) {
|
||||
fmt.Fprintln(p.out, append([]interface{}{m}, args...)...)
|
||||
}
|
||||
|
||||
@@ -95,6 +95,12 @@ func (w *ttyWriter) Event(e Event) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Events(events []Event) {
|
||||
for _, e := range events {
|
||||
w.Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) TailMsgf(msg string, args ...interface{}) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
|
||||
@@ -31,6 +31,7 @@ type Writer interface {
|
||||
Start(context.Context) error
|
||||
Stop()
|
||||
Event(Event)
|
||||
Events([]Event)
|
||||
TailMsgf(string, ...interface{})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StringContains check if an array contains a specific value
|
||||
func StringContains(array []string, needle string) bool {
|
||||
for _, val := range array {
|
||||
@@ -25,3 +30,9 @@ func StringContains(array []string, needle string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StringToBool converts a string to a boolean ignoring errors
|
||||
func StringToBool(s string) bool {
|
||||
b, _ := strconv.ParseBool(strings.ToLower(strings.TrimSpace(s)))
|
||||
return b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user