mirror of
https://github.com/docker/compose.git
synced 2026-02-14 20:49:24 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6f842b042 | ||
|
|
4fbbf201cd | ||
|
|
935d72f46f | ||
|
|
41682acc77 | ||
|
|
1054792b47 | ||
|
|
19f66918cc | ||
|
|
186744e034 | ||
|
|
bc9d696fa0 | ||
|
|
6204fb1c94 | ||
|
|
5d732010a7 | ||
|
|
2006f3fe7d | ||
|
|
192718c001 | ||
|
|
c79f67fead | ||
|
|
3b294bfdda | ||
|
|
dd34f7a22b | ||
|
|
caad72713b |
@@ -17,7 +17,7 @@
|
||||
|
||||
ARG GO_VERSION=1.21.0
|
||||
ARG XX_VERSION=1.2.1
|
||||
ARG GOLANGCI_LINT_VERSION=v1.53.2
|
||||
ARG GOLANGCI_LINT_VERSION=v1.54.2
|
||||
ARG ADDLICENSE_VERSION=v1.0.0
|
||||
|
||||
ARG BUILD_TAGS="e2e"
|
||||
@@ -89,10 +89,13 @@ RUN --mount=type=bind,target=. \
|
||||
|
||||
FROM build-base AS lint
|
||||
ARG BUILD_TAGS
|
||||
ENV GOLANGCI_LINT_CACHE=/cache/golangci-lint
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/cache/golangci-lint \
|
||||
--mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
|
||||
golangci-lint cache status && \
|
||||
golangci-lint run --build-tags "$BUILD_TAGS" ./...
|
||||
|
||||
FROM build-base AS test
|
||||
|
||||
@@ -84,4 +84,4 @@ If you find an issue, please report it on the
|
||||
Legacy
|
||||
-------------
|
||||
|
||||
The Python version of Compose is available under the `v1` [branch](https://github.com/docker/compose/tree/v1)
|
||||
The Python version of Compose is available under the `v1` [branch](https://github.com/docker/compose/tree/v1).
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/compose-spec/compose-go/dotenv"
|
||||
buildx "github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/remote"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
@@ -134,7 +135,25 @@ func (o *ProjectOptions) WithProject(fn ProjectFunc) func(cmd *cobra.Command, ar
|
||||
// WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
|
||||
func (o *ProjectOptions) WithServices(fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
|
||||
return Adapt(func(ctx context.Context, args []string) error {
|
||||
project, err := o.ToProject(args, cli.WithResolvedPaths(true), cli.WithDiscardEnvFile)
|
||||
options := []cli.ProjectOptionsFn{
|
||||
cli.WithResolvedPaths(true),
|
||||
cli.WithDiscardEnvFile,
|
||||
cli.WithContext(ctx),
|
||||
}
|
||||
|
||||
enabled, err := remote.GitRemoteLoaderEnabled()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if enabled {
|
||||
git, err := remote.NewGitRemoteLoader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options = append(options, cli.WithResourceLoader(git))
|
||||
}
|
||||
|
||||
project, err := o.ToProject(args, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -243,7 +262,7 @@ func (o *ProjectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.Proj
|
||||
cli.WithDotEnv,
|
||||
cli.WithConfigFileEnv,
|
||||
cli.WithDefaultConfigPath,
|
||||
cli.WithProfiles(o.Profiles),
|
||||
cli.WithDefaultProfiles(o.Profiles...),
|
||||
cli.WithName(o.ProjectName))...)
|
||||
}
|
||||
|
||||
@@ -256,7 +275,7 @@ func RunningAsStandalone() bool {
|
||||
}
|
||||
|
||||
// RootCommand returns the compose command with its child commands
|
||||
func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo
|
||||
func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo
|
||||
// filter out useless commandConn.CloseWrite warning message that can occur
|
||||
// when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
|
||||
// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
|
||||
@@ -288,7 +307,7 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
|
||||
return cmd.Help()
|
||||
}
|
||||
if version {
|
||||
return versionCommand(streams).Execute()
|
||||
return versionCommand(dockerCli).Execute()
|
||||
}
|
||||
_ = cmd.Help()
|
||||
return dockercli.StatusError{
|
||||
@@ -326,11 +345,11 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
|
||||
ansi = v
|
||||
}
|
||||
|
||||
formatter.SetANSIMode(streams, ansi)
|
||||
formatter.SetANSIMode(dockerCli, ansi)
|
||||
|
||||
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
|
||||
ui.NoColor()
|
||||
formatter.SetANSIMode(streams, formatter.Never)
|
||||
formatter.SetANSIMode(dockerCli, formatter.Never)
|
||||
}
|
||||
|
||||
switch ansi {
|
||||
@@ -407,26 +426,26 @@ func RootCommand(streams command.Cli, backend api.Service) *cobra.Command { //no
|
||||
}
|
||||
|
||||
c.AddCommand(
|
||||
upCommand(&opts, streams, backend),
|
||||
upCommand(&opts, dockerCli, backend),
|
||||
downCommand(&opts, backend),
|
||||
startCommand(&opts, backend),
|
||||
restartCommand(&opts, backend),
|
||||
stopCommand(&opts, backend),
|
||||
psCommand(&opts, streams, backend),
|
||||
listCommand(streams, backend),
|
||||
logsCommand(&opts, streams, backend),
|
||||
configCommand(&opts, streams, backend),
|
||||
psCommand(&opts, dockerCli, backend),
|
||||
listCommand(dockerCli, backend),
|
||||
logsCommand(&opts, dockerCli, backend),
|
||||
configCommand(&opts, dockerCli, backend),
|
||||
killCommand(&opts, backend),
|
||||
runCommand(&opts, streams, backend),
|
||||
runCommand(&opts, dockerCli, backend),
|
||||
removeCommand(&opts, backend),
|
||||
execCommand(&opts, streams, backend),
|
||||
execCommand(&opts, dockerCli, backend),
|
||||
pauseCommand(&opts, backend),
|
||||
unpauseCommand(&opts, backend),
|
||||
topCommand(&opts, streams, backend),
|
||||
eventsCommand(&opts, streams, backend),
|
||||
portCommand(&opts, streams, backend),
|
||||
imagesCommand(&opts, streams, backend),
|
||||
versionCommand(streams),
|
||||
topCommand(&opts, dockerCli, backend),
|
||||
eventsCommand(&opts, dockerCli, backend),
|
||||
portCommand(&opts, dockerCli, backend),
|
||||
imagesCommand(&opts, dockerCli, backend),
|
||||
versionCommand(dockerCli),
|
||||
buildCommand(&opts, &progress, backend),
|
||||
pushCommand(&opts, backend),
|
||||
pullCommand(&opts, backend),
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/remote"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
@@ -49,14 +50,21 @@ type configOptions struct {
|
||||
noConsistency bool
|
||||
}
|
||||
|
||||
func (o *configOptions) ToProject(services []string) (*types.Project, error) {
|
||||
func (o *configOptions) ToProject(ctx context.Context, services []string) (*types.Project, error) {
|
||||
git, err := remote.NewGitRemoteLoader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o.ProjectOptions.ToProject(services,
|
||||
cli.WithInterpolation(!o.noInterpolate),
|
||||
cli.WithResolvedPaths(!o.noResolvePath),
|
||||
cli.WithNormalization(!o.noNormalize),
|
||||
cli.WithConsistency(!o.noConsistency),
|
||||
cli.WithProfiles(o.Profiles),
|
||||
cli.WithDiscardEnvFile)
|
||||
cli.WithDefaultProfiles(o.Profiles...),
|
||||
cli.WithDiscardEnvFile,
|
||||
cli.WithContext(ctx),
|
||||
cli.WithResourceLoader(git))
|
||||
}
|
||||
|
||||
func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
|
||||
@@ -82,19 +90,19 @@ func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service)
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
if opts.services {
|
||||
return runServices(streams, opts)
|
||||
return runServices(ctx, streams, opts)
|
||||
}
|
||||
if opts.volumes {
|
||||
return runVolumes(streams, opts)
|
||||
return runVolumes(ctx, streams, opts)
|
||||
}
|
||||
if opts.hash != "" {
|
||||
return runHash(streams, opts)
|
||||
return runHash(ctx, streams, opts)
|
||||
}
|
||||
if opts.profiles {
|
||||
return runProfiles(streams, opts, args)
|
||||
return runProfiles(ctx, streams, opts, args)
|
||||
}
|
||||
if opts.images {
|
||||
return runConfigImages(streams, opts, args)
|
||||
return runConfigImages(ctx, streams, opts, args)
|
||||
}
|
||||
|
||||
return runConfig(ctx, streams, backend, opts, args)
|
||||
@@ -122,7 +130,7 @@ func configCommand(p *ProjectOptions, streams api.Streams, backend api.Service)
|
||||
|
||||
func runConfig(ctx context.Context, streams api.Streams, backend api.Service, opts configOptions, services []string) error {
|
||||
var content []byte
|
||||
project, err := opts.ToProject(services)
|
||||
project, err := opts.ToProject(ctx, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -151,8 +159,8 @@ func runConfig(ctx context.Context, streams api.Streams, backend api.Service, op
|
||||
return err
|
||||
}
|
||||
|
||||
func runServices(streams api.Streams, opts configOptions) error {
|
||||
project, err := opts.ToProject(nil)
|
||||
func runServices(ctx context.Context, streams api.Streams, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -162,8 +170,8 @@ func runServices(streams api.Streams, opts configOptions) error {
|
||||
})
|
||||
}
|
||||
|
||||
func runVolumes(streams api.Streams, opts configOptions) error {
|
||||
project, err := opts.ToProject(nil)
|
||||
func runVolumes(ctx context.Context, streams api.Streams, opts configOptions) error {
|
||||
project, err := opts.ToProject(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -173,12 +181,12 @@ func runVolumes(streams api.Streams, opts configOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHash(streams api.Streams, opts configOptions) error {
|
||||
func runHash(ctx context.Context, streams api.Streams, opts configOptions) error {
|
||||
var services []string
|
||||
if opts.hash != "*" {
|
||||
services = append(services, strings.Split(opts.hash, ",")...)
|
||||
}
|
||||
project, err := opts.ToProject(nil)
|
||||
project, err := opts.ToProject(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,9 +213,9 @@ func runHash(streams api.Streams, opts configOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runProfiles(streams api.Streams, opts configOptions, services []string) error {
|
||||
func runProfiles(ctx context.Context, streams api.Streams, opts configOptions, services []string) error {
|
||||
set := map[string]struct{}{}
|
||||
project, err := opts.ToProject(services)
|
||||
project, err := opts.ToProject(ctx, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -227,8 +235,8 @@ func runProfiles(streams api.Streams, opts configOptions, services []string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func runConfigImages(streams api.Streams, opts configOptions, services []string) error {
|
||||
project, err := opts.ToProject(services)
|
||||
func runConfigImages(ctx context.Context, streams api.Streams, opts configOptions, services []string) error {
|
||||
project, err := opts.ToProject(ctx, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,22 +19,18 @@ package compose
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/api/types"
|
||||
|
||||
formatter2 "github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/docker/cli/cli/command"
|
||||
cliformatter "github.com/docker/cli/cli/command/formatter"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type psOptions struct {
|
||||
@@ -66,7 +62,7 @@ func (p *psOptions) parseFilter() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
|
||||
func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
||||
opts := psOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
@@ -77,12 +73,12 @@ func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
|
||||
return opts.parseFilter()
|
||||
},
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runPs(ctx, streams, backend, args, opts)
|
||||
return runPs(ctx, dockerCli, backend, args, opts)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
flags := psCmd.Flags()
|
||||
flags.StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]")
|
||||
flags.StringVar(&opts.Format, "format", "table", cliflags.FormatHelp)
|
||||
flags.StringVar(&opts.Filter, "filter", "", "Filter services by a property (supported filters: status).")
|
||||
flags.StringArrayVar(&opts.Status, "status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
|
||||
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
@@ -91,7 +87,7 @@ func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
|
||||
return psCmd
|
||||
}
|
||||
|
||||
func runPs(ctx context.Context, streams api.Streams, backend api.Service, services []string, opts psOptions) error {
|
||||
func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error {
|
||||
project, name, err := opts.projectOrName(services...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -125,38 +121,32 @@ func runPs(ctx context.Context, streams api.Streams, backend api.Service, servic
|
||||
|
||||
if opts.Quiet {
|
||||
for _, c := range containers {
|
||||
fmt.Fprintln(streams.Out(), c.ID)
|
||||
fmt.Fprintln(dockerCli.Out(), c.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.Services {
|
||||
services := []string{}
|
||||
for _, s := range containers {
|
||||
if !utils.StringContains(services, s.Service) {
|
||||
services = append(services, s.Service)
|
||||
for _, c := range containers {
|
||||
s := c.Service
|
||||
if !utils.StringContains(services, s) {
|
||||
services = append(services, s)
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(streams.Out(), strings.Join(services, "\n"))
|
||||
fmt.Fprintln(dockerCli.Out(), strings.Join(services, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
return formatter.Print(containers, opts.Format, streams.Out(),
|
||||
writer(containers),
|
||||
"NAME", "IMAGE", "COMMAND", "SERVICE", "CREATED", "STATUS", "PORTS")
|
||||
}
|
||||
|
||||
func writer(containers []api.ContainerSummary) func(w io.Writer) {
|
||||
return func(w io.Writer) {
|
||||
for _, container := range containers {
|
||||
ports := displayablePorts(container)
|
||||
createdAt := time.Unix(container.Created, 0)
|
||||
created := units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||
status := container.Status
|
||||
command := formatter2.Ellipsis(container.Command, 20)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", container.Name, container.Image, strconv.Quote(command), container.Service, created, status, ports)
|
||||
}
|
||||
if opts.Format == "" {
|
||||
opts.Format = dockerCli.ConfigFile().PsFormat
|
||||
}
|
||||
|
||||
containerCtx := cliformatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewContainerFormat(opts.Format, opts.Quiet, false),
|
||||
}
|
||||
return formatter.ContainerWrite(containerCtx, containers)
|
||||
}
|
||||
|
||||
func filterByStatus(containers []api.ContainerSummary, statuses []string) []api.ContainerSummary {
|
||||
@@ -177,21 +167,3 @@ func hasStatus(c api.ContainerSummary, statuses []string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func displayablePorts(c api.ContainerSummary) string {
|
||||
if c.Publishers == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ports := make([]types.Port, len(c.Publishers))
|
||||
for i, pub := range c.Publishers {
|
||||
ports[i] = types.Port{
|
||||
IP: pub.URL,
|
||||
PrivatePort: uint16(pub.TargetPort),
|
||||
PublicPort: uint16(pub.PublishedPort),
|
||||
Type: pub.Protocol,
|
||||
}
|
||||
}
|
||||
|
||||
return formatter2.DisplayablePorts(ports)
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/mocks"
|
||||
@@ -69,7 +69,11 @@ func TestPsTable(t *testing.T) {
|
||||
}).AnyTimes()
|
||||
|
||||
opts := psOptions{ProjectOptions: &ProjectOptions{ProjectName: "test"}}
|
||||
err = runPs(ctx, stream{out: streams.NewOut(f)}, backend, nil, opts)
|
||||
stdout := streams.NewOut(f)
|
||||
cli := mocks.NewMockCli(ctrl)
|
||||
cli.EXPECT().Out().Return(stdout).AnyTimes()
|
||||
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
|
||||
err = runPs(ctx, cli, backend, nil, opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = f.Seek(0, 0)
|
||||
@@ -80,21 +84,3 @@ func TestPsTable(t *testing.T) {
|
||||
|
||||
assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
out *streams.Out
|
||||
err io.Writer
|
||||
in *streams.In
|
||||
}
|
||||
|
||||
func (s stream) Out() *streams.Out {
|
||||
return s.out
|
||||
}
|
||||
|
||||
func (s stream) Err() io.Writer {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s stream) In() *streams.In {
|
||||
return s.in
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
@@ -83,7 +85,10 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
|
||||
RunE: p.WithServices(func(ctx context.Context, project *types.Project, services []string) error {
|
||||
create.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
|
||||
if create.ignoreOrphans && create.removeOrphans {
|
||||
return fmt.Errorf("%s and --remove-orphans cannot be combined", ComposeIgnoreOrphans)
|
||||
return fmt.Errorf("cannot combine %s and --remove-orphans", ComposeIgnoreOrphans)
|
||||
}
|
||||
if len(up.attach) != 0 && up.attachDependencies {
|
||||
return errors.New("cannot combine --attach and --attach-dependencies")
|
||||
}
|
||||
return runUp(ctx, streams, backend, create, up, project, services)
|
||||
}),
|
||||
@@ -108,12 +113,12 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
|
||||
flags.BoolVar(&up.noDeps, "no-deps", false, "Don't start linked services.")
|
||||
flags.BoolVar(&create.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
|
||||
flags.BoolVarP(&create.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers.")
|
||||
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Attach to dependent containers.")
|
||||
flags.BoolVar(&create.quietPull, "quiet-pull", false, "Pull without printing progress information.")
|
||||
flags.StringArrayVar(&up.attach, "attach", []string{}, "Attach to service output.")
|
||||
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Don't attach to specified service.")
|
||||
flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.")
|
||||
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services.")
|
||||
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services.")
|
||||
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
|
||||
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "timeout waiting for application to be running|healthy.")
|
||||
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration to wait for the project to be running|healthy.")
|
||||
|
||||
return upCmd
|
||||
}
|
||||
@@ -158,37 +163,6 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
|
||||
return err
|
||||
}
|
||||
|
||||
var consumer api.LogConsumer
|
||||
if !upOptions.Detach {
|
||||
consumer = formatter.NewLogConsumer(ctx, streams.Out(), streams.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)
|
||||
}
|
||||
|
||||
attachTo := utils.Set[string]{}
|
||||
if len(upOptions.attach) > 0 {
|
||||
attachTo.AddAll(upOptions.attach...)
|
||||
}
|
||||
if upOptions.attachDependencies {
|
||||
if err := project.WithServices(attachTo.Elements(), func(s types.ServiceConfig) error {
|
||||
if s.Attach == nil || *s.Attach {
|
||||
attachTo.Add(s.Name)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(attachTo) == 0 {
|
||||
if err := project.WithServices(services, func(s types.ServiceConfig) error {
|
||||
if s.Attach == nil || *s.Attach {
|
||||
attachTo.Add(s.Name)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
attachTo.RemoveAll(upOptions.noAttach...)
|
||||
|
||||
create := api.CreateOptions{
|
||||
Services: services,
|
||||
RemoveOrphans: createOptions.removeOrphans,
|
||||
@@ -204,14 +178,48 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
|
||||
return backend.Create(ctx, project, create)
|
||||
}
|
||||
|
||||
timeout := time.Duration(upOptions.waitTimeout) * time.Second
|
||||
var consumer api.LogConsumer
|
||||
var attach []string
|
||||
if !upOptions.Detach {
|
||||
consumer = formatter.NewLogConsumer(ctx, streams.Out(), streams.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)
|
||||
|
||||
var attachSet utils.Set[string]
|
||||
if len(upOptions.attach) != 0 {
|
||||
// services are passed explicitly with --attach, verify they're valid and then use them as-is
|
||||
attachSet = utils.NewSet(upOptions.attach...)
|
||||
unexpectedSvcs := attachSet.Diff(utils.NewSet(project.ServiceNames()...))
|
||||
if len(unexpectedSvcs) != 0 {
|
||||
return fmt.Errorf("cannot attach to services not included in up: %s", strings.Join(unexpectedSvcs.Elements(), ", "))
|
||||
}
|
||||
} else {
|
||||
// mark services being launched (and potentially their deps) for attach
|
||||
// if they didn't opt-out via Compose YAML
|
||||
attachSet = utils.NewSet[string]()
|
||||
var dependencyOpt types.DependencyOption = types.IgnoreDependencies
|
||||
if upOptions.attachDependencies {
|
||||
dependencyOpt = types.IncludeDependencies
|
||||
}
|
||||
if err := project.WithServices(services, func(s types.ServiceConfig) error {
|
||||
if s.Attach == nil || *s.Attach {
|
||||
attachSet.Add(s.Name)
|
||||
}
|
||||
return nil
|
||||
}, dependencyOpt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// filter out any services that have been explicitly marked for ignore with `--no-attach`
|
||||
attachSet.RemoveAll(upOptions.noAttach...)
|
||||
attach = attachSet.Elements()
|
||||
}
|
||||
|
||||
timeout := time.Duration(upOptions.waitTimeout) * time.Second
|
||||
return backend.Up(ctx, project, api.UpOptions{
|
||||
Create: create,
|
||||
Start: api.StartOptions{
|
||||
Project: project,
|
||||
Attach: consumer,
|
||||
AttachTo: attachTo.Elements(),
|
||||
AttachTo: attach,
|
||||
ExitCodeFrom: upOptions.exitCodeFrom,
|
||||
CascadeStop: upOptions.cascadeStop,
|
||||
Wait: upOptions.wait,
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/compose/v2/internal/locker"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -57,5 +59,13 @@ func runWatch(ctx context.Context, backend api.Service, opts watchOptions, servi
|
||||
return err
|
||||
}
|
||||
|
||||
l, err := locker.NewPidfile(project.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
|
||||
}
|
||||
if err := l.Lock(); err != nil {
|
||||
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
|
||||
}
|
||||
|
||||
return backend.Watch(ctx, project, services, api.WatchOptions{})
|
||||
}
|
||||
|
||||
281
cmd/formatter/container.go
Normal file
281
cmd/formatter/container.go
Normal file
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
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 formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultContainerTableFormat = "table {{.Name}}\t{{.Image}}\t{{.Command}}\t{{.Service}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
nameHeader = "NAME"
|
||||
serviceHeader = "SERVICE"
|
||||
commandHeader = "COMMAND"
|
||||
runningForHeader = "CREATED"
|
||||
mountsHeader = "MOUNTS"
|
||||
localVolumes = "LOCAL VOLUMES"
|
||||
networksHeader = "NETWORKS"
|
||||
)
|
||||
|
||||
// NewContainerFormat returns a Format for rendering using a Context
|
||||
func NewContainerFormat(source string, quiet bool, size bool) formatter.Format {
|
||||
switch source {
|
||||
case formatter.TableFormatKey, "": // table formatting is the default if none is set.
|
||||
if quiet {
|
||||
return formatter.DefaultQuietFormat
|
||||
}
|
||||
format := defaultContainerTableFormat
|
||||
if size {
|
||||
format += `\t{{.Size}}`
|
||||
}
|
||||
return formatter.Format(format)
|
||||
case formatter.RawFormatKey:
|
||||
if quiet {
|
||||
return `container_id: {{.ID}}`
|
||||
}
|
||||
format := `container_id: {{.ID}}
|
||||
image: {{.Image}}
|
||||
command: {{.Command}}
|
||||
created_at: {{.CreatedAt}}
|
||||
state: {{- pad .State 1 0}}
|
||||
status: {{- pad .Status 1 0}}
|
||||
names: {{.Names}}
|
||||
labels: {{- pad .Labels 1 0}}
|
||||
ports: {{- pad .Ports 1 0}}
|
||||
`
|
||||
if size {
|
||||
format += `size: {{.Size}}\n`
|
||||
}
|
||||
return formatter.Format(format)
|
||||
default: // custom format
|
||||
if quiet {
|
||||
return formatter.DefaultQuietFormat
|
||||
}
|
||||
return formatter.Format(source)
|
||||
}
|
||||
}
|
||||
|
||||
// ContainerWrite renders the context for a list of containers
|
||||
func ContainerWrite(ctx formatter.Context, containers []api.ContainerSummary) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, container := range containers {
|
||||
err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ctx.Write(NewContainerContext(), render)
|
||||
}
|
||||
|
||||
// ContainerContext is a struct used for rendering a list of containers in a Go template.
|
||||
type ContainerContext struct {
|
||||
formatter.HeaderContext
|
||||
trunc bool
|
||||
c api.ContainerSummary
|
||||
|
||||
// FieldsUsed is used in the pre-processing step to detect which fields are
|
||||
// used in the template. It's currently only used to detect use of the .Size
|
||||
// field which (if used) automatically sets the '--size' option when making
|
||||
// the API call.
|
||||
FieldsUsed map[string]interface{}
|
||||
}
|
||||
|
||||
// NewContainerContext creates a new context for rendering containers
|
||||
func NewContainerContext() *ContainerContext {
|
||||
containerCtx := ContainerContext{}
|
||||
containerCtx.Header = formatter.SubHeaderContext{
|
||||
"ID": formatter.ContainerIDHeader,
|
||||
"Name": nameHeader,
|
||||
"Service": serviceHeader,
|
||||
"Image": formatter.ImageHeader,
|
||||
"Command": commandHeader,
|
||||
"CreatedAt": formatter.CreatedAtHeader,
|
||||
"RunningFor": runningForHeader,
|
||||
"Ports": formatter.PortsHeader,
|
||||
"State": formatter.StateHeader,
|
||||
"Status": formatter.StatusHeader,
|
||||
"Size": formatter.SizeHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
}
|
||||
return &containerCtx
|
||||
}
|
||||
|
||||
// MarshalJSON makes ContainerContext implement json.Marshaler
|
||||
func (c *ContainerContext) MarshalJSON() ([]byte, error) {
|
||||
return formatter.MarshalJSON(c)
|
||||
}
|
||||
|
||||
// ID returns the container's ID as a string. Depending on the `--no-trunc`
|
||||
// option being set, the full or truncated ID is returned.
|
||||
func (c *ContainerContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.c.ID)
|
||||
}
|
||||
return c.c.ID
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Name() string {
|
||||
return c.c.Name
|
||||
}
|
||||
|
||||
// Names returns a comma-separated string of the container's names, with their
|
||||
// slash (/) prefix stripped. Additional names for the container (related to the
|
||||
// legacy `--link` feature) are omitted.
|
||||
func (c *ContainerContext) Names() string {
|
||||
names := formatter.StripNamePrefix(c.c.Names)
|
||||
if c.trunc {
|
||||
for _, name := range names {
|
||||
if len(strings.Split(name, "/")) == 1 {
|
||||
names = []string{name}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(names, ",")
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Service() string {
|
||||
return c.c.Service
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Image() string {
|
||||
return c.c.Image
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Command() string {
|
||||
command := c.c.Command
|
||||
if c.trunc {
|
||||
command = formatter.Ellipsis(command, 20)
|
||||
}
|
||||
return strconv.Quote(command)
|
||||
}
|
||||
|
||||
func (c *ContainerContext) CreatedAt() string {
|
||||
return time.Unix(c.c.Created, 0).String()
|
||||
}
|
||||
|
||||
func (c *ContainerContext) RunningFor() string {
|
||||
createdAt := time.Unix(c.c.Created, 0)
|
||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *ContainerContext) ExitCode() int {
|
||||
return c.c.ExitCode
|
||||
}
|
||||
|
||||
func (c *ContainerContext) State() string {
|
||||
return c.c.State
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Status() string {
|
||||
return c.c.Status
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Health() string {
|
||||
return c.c.Health
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Publishers() api.PortPublishers {
|
||||
return c.c.Publishers
|
||||
}
|
||||
|
||||
func (c *ContainerContext) Ports() string {
|
||||
var ports []types.Port
|
||||
for _, publisher := range c.c.Publishers {
|
||||
ports = append(ports, types.Port{
|
||||
IP: publisher.URL,
|
||||
PrivatePort: uint16(publisher.TargetPort),
|
||||
PublicPort: uint16(publisher.PublishedPort),
|
||||
Type: publisher.Protocol,
|
||||
})
|
||||
}
|
||||
return formatter.DisplayablePorts(ports)
|
||||
}
|
||||
|
||||
// Labels returns a comma-separated string of labels present on the container.
|
||||
func (c *ContainerContext) Labels() string {
|
||||
if c.c.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var joinLabels []string
|
||||
for k, v := range c.c.Labels {
|
||||
joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(joinLabels, ",")
|
||||
}
|
||||
|
||||
// Label returns the value of the label with the given name or an empty string
|
||||
// if the given label does not exist.
|
||||
func (c *ContainerContext) Label(name string) string {
|
||||
if c.c.Labels == nil {
|
||||
return ""
|
||||
}
|
||||
return c.c.Labels[name]
|
||||
}
|
||||
|
||||
// Mounts returns a comma-separated string of mount names present on the container.
|
||||
// If the trunc option is set, names can be truncated (ellipsized).
|
||||
func (c *ContainerContext) Mounts() string {
|
||||
var mounts []string
|
||||
for _, name := range c.c.Mounts {
|
||||
if c.trunc {
|
||||
name = formatter.Ellipsis(name, 15)
|
||||
}
|
||||
mounts = append(mounts, name)
|
||||
}
|
||||
return strings.Join(mounts, ",")
|
||||
}
|
||||
|
||||
// LocalVolumes returns the number of volumes using the "local" volume driver.
|
||||
func (c *ContainerContext) LocalVolumes() string {
|
||||
return fmt.Sprintf("%d", c.c.LocalVolumes)
|
||||
}
|
||||
|
||||
// Networks returns a comma-separated string of networks that the container is
|
||||
// attached to.
|
||||
func (c *ContainerContext) Networks() string {
|
||||
return strings.Join(c.c.Networks, ",")
|
||||
}
|
||||
|
||||
// Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
|
||||
func (c *ContainerContext) Size() string {
|
||||
if c.FieldsUsed == nil {
|
||||
c.FieldsUsed = map[string]interface{}{}
|
||||
}
|
||||
c.FieldsUsed["Size"] = struct{}{}
|
||||
srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
|
||||
sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
|
||||
|
||||
sf := srw
|
||||
if c.c.SizeRootFs > 0 {
|
||||
sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
|
||||
}
|
||||
return sf
|
||||
}
|
||||
@@ -5,15 +5,15 @@ List containers
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------------|:--------------|:--------|:--------------------------------------------------------------------------------------------------------------|
|
||||
| `-a`, `--all` | | | Show all stopped containers (including those created by the run command) |
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). |
|
||||
| [`--format`](#format) | `string` | `table` | Format the output. Values: [table \| json] |
|
||||
| `-q`, `--quiet` | | | Only display IDs |
|
||||
| `--services` | | | Display services |
|
||||
| [`--status`](#status) | `stringArray` | | Filter services by status. Values: [paused \| restarting \| removing \| running \| dead \| created \| exited] |
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------------|:--------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `-a`, `--all` | | | Show all stopped containers (including those created by the run command) |
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
| [`--filter`](#filter) | `string` | | Filter services by a property (supported filters: status). |
|
||||
| [`--format`](#format) | `string` | `table` | Format output using a custom template:<br>'table': Print output in table format with column headers (default)<br>'table TEMPLATE': Print output in table format using the given Go template<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
|
||||
| `-q`, `--quiet` | | | Only display IDs |
|
||||
| `--services` | | | Display services |
|
||||
| [`--status`](#status) | `stringArray` | | Filter services by status. Values: [paused \| restarting \| removing \| running \| dead \| created \| exited] |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -9,14 +9,14 @@ Create and start containers
|
||||
|:-----------------------------|:--------------|:----------|:---------------------------------------------------------------------------------------------------------|
|
||||
| `--abort-on-container-exit` | | | Stops all containers if any container was stopped. Incompatible with -d |
|
||||
| `--always-recreate-deps` | | | Recreate dependent containers. Incompatible with --no-recreate. |
|
||||
| `--attach` | `stringArray` | | Attach to service output. |
|
||||
| `--attach-dependencies` | | | Attach to dependent containers. |
|
||||
| `--attach` | `stringArray` | | Restrict attaching to the specified services. Incompatible with --attach-dependencies. |
|
||||
| `--attach-dependencies` | | | Automatically attach to log output of dependent services. |
|
||||
| `--build` | | | Build images before starting containers. |
|
||||
| `-d`, `--detach` | | | Detached mode: Run containers in the background |
|
||||
| `--dry-run` | | | Execute command in dry run mode |
|
||||
| `--exit-code-from` | `string` | | Return the exit code of the selected service container. Implies --abort-on-container-exit |
|
||||
| `--force-recreate` | | | Recreate containers even if their configuration and image haven't changed. |
|
||||
| `--no-attach` | `stringArray` | | Don't attach to specified service. |
|
||||
| `--no-attach` | `stringArray` | | Do not attach (stream logs) to the specified services. |
|
||||
| `--no-build` | | | Don't build an image, even if it's missing. |
|
||||
| `--no-color` | | | Produce monochrome output. |
|
||||
| `--no-deps` | | | Don't start linked services. |
|
||||
@@ -31,7 +31,7 @@ Create and start containers
|
||||
| `-t`, `--timeout` | `int` | `0` | Use this timeout in seconds for container shutdown when attached or when containers are already running. |
|
||||
| `--timestamps` | | | Show timestamps. |
|
||||
| `--wait` | | | Wait for services to be running\|healthy. Implies detached mode. |
|
||||
| `--wait-timeout` | `int` | `0` | timeout waiting for application to be running\|healthy. |
|
||||
| `--wait-timeout` | `int` | `0` | Maximum duration to wait for the project to be running\|healthy. |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -46,7 +46,13 @@ options:
|
||||
- option: format
|
||||
value_type: string
|
||||
default_value: table
|
||||
description: 'Format the output. Values: [table | json]'
|
||||
description: |-
|
||||
Format output using a custom template:
|
||||
'table': Print output in table format with column headers (default)
|
||||
'table TEMPLATE': Print output in table format using the given Go template
|
||||
'json': Print in JSON format
|
||||
'TEMPLATE': Print output using the given Go template.
|
||||
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates
|
||||
details_url: '#format'
|
||||
deprecated: false
|
||||
hidden: false
|
||||
|
||||
@@ -48,7 +48,8 @@ options:
|
||||
- option: attach
|
||||
value_type: stringArray
|
||||
default_value: '[]'
|
||||
description: Attach to service output.
|
||||
description: |
|
||||
Restrict attaching to the specified services. Incompatible with --attach-dependencies.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
@@ -58,7 +59,7 @@ options:
|
||||
- option: attach-dependencies
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Attach to dependent containers.
|
||||
description: Automatically attach to log output of dependent services.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
@@ -110,7 +111,7 @@ options:
|
||||
- option: no-attach
|
||||
value_type: stringArray
|
||||
default_value: '[]'
|
||||
description: Don't attach to specified service.
|
||||
description: Do not attach (stream logs) to the specified services.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
@@ -266,7 +267,7 @@ options:
|
||||
- option: wait-timeout
|
||||
value_type: int
|
||||
default_value: "0"
|
||||
description: timeout waiting for application to be running|healthy.
|
||||
description: Maximum duration to wait for the project to be running|healthy.
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
|
||||
9
go.mod
9
go.mod
@@ -5,8 +5,9 @@ go 1.21
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/buger/goterm v1.0.4
|
||||
github.com/compose-spec/compose-go v1.18.1
|
||||
github.com/compose-spec/compose-go v1.18.3
|
||||
github.com/containerd/console v1.0.3
|
||||
github.com/containerd/containerd v1.7.3
|
||||
github.com/cucumber/godog v0.0.0-00010101000000-000000000000 // replaced; see replace for the actual version used
|
||||
@@ -14,7 +15,7 @@ require (
|
||||
github.com/docker/buildx v0.11.2
|
||||
github.com/docker/cli v24.0.5+incompatible
|
||||
github.com/docker/cli-docs-tool v0.6.0
|
||||
github.com/docker/docker v24.0.5+incompatible // v24.0.5-dev
|
||||
github.com/docker/docker v24.0.5+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/fsnotify/fsevents v0.1.1
|
||||
@@ -24,8 +25,8 @@ require (
|
||||
github.com/jonboulle/clockwork v0.4.0
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/moby/buildkit v0.12.1 // v0.12 release branch
|
||||
github.com/moby/patternmatcher v0.5.0
|
||||
github.com/moby/buildkit v0.12.1
|
||||
github.com/moby/patternmatcher v0.6.0
|
||||
github.com/moby/term v0.5.0
|
||||
github.com/morikuni/aec v1.0.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
|
||||
10
go.sum
10
go.sum
@@ -66,6 +66,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
||||
@@ -141,8 +143,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/compose-spec/compose-go v1.18.1 h1:YVYYkV8fAHW/eCOgtqSe1tHrlaDVvwS8zgs6F5ukm/Y=
|
||||
github.com/compose-spec/compose-go v1.18.1/go.mod h1:zR2tP1+kZHi5vJz7PjpW6oMoDji/Js3GHjP+hfjf70Q=
|
||||
github.com/compose-spec/compose-go v1.18.3 h1:hiwTZ8ED1l+CB2G2G4LFv/bIaoUfG2ZBalz4S7MOy5w=
|
||||
github.com/compose-spec/compose-go v1.18.3/go.mod h1:zR2tP1+kZHi5vJz7PjpW6oMoDji/Js3GHjP+hfjf70Q=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
@@ -495,8 +497,8 @@ github.com/moby/buildkit v0.12.1 h1:vvMG7EZYCiQZpTtXQkvyeyj7HzT1JHhDWj+/aiGIzLM=
|
||||
github.com/moby/buildkit v0.12.1/go.mod h1:adB4y0SxxX8trnrY+oEulb48ODLqPO6pKMF0ppGcCoI=
|
||||
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
|
||||
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||
|
||||
41
internal/locker/pidfile.go
Normal file
41
internal/locker/pidfile.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2023 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 locker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/docker/docker/pkg/pidfile"
|
||||
)
|
||||
|
||||
type Pidfile struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewPidfile(projectName string) (*Pidfile, error) {
|
||||
path, err := xdg.RuntimeFile(fmt.Sprintf("docker-compose.%s.pid", projectName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Pidfile{path: path}, nil
|
||||
}
|
||||
|
||||
func (f *Pidfile) Lock() error {
|
||||
return pidfile.Write(f.path, os.Getpid())
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -73,6 +75,7 @@ func ProjectOptions(proj *types.Project) SpanOptions {
|
||||
attribute.StringSlice("project.secrets", proj.SecretNames()),
|
||||
attribute.StringSlice("project.configs", proj.ConfigNames()),
|
||||
attribute.StringSlice("project.extensions", keys(proj.Extensions)),
|
||||
attribute.StringSlice("project.includes", flattenIncludeReferences(proj.IncludeReferences)),
|
||||
}
|
||||
return []trace.SpanStartEventOption{
|
||||
trace.WithAttributes(attrs...),
|
||||
@@ -150,3 +153,13 @@ func timeAttr(key string, value time.Time) attribute.KeyValue {
|
||||
func unixTimeAttr(key string, value int64) attribute.KeyValue {
|
||||
return timeAttr(key, time.Unix(value, 0).UTC())
|
||||
}
|
||||
|
||||
func flattenIncludeReferences(includeRefs map[string][]types.IncludeConfig) []string {
|
||||
ret := utils.NewSet[string]()
|
||||
for _, included := range includeRefs {
|
||||
for i := range included {
|
||||
ret.AddAll(included[i].Path...)
|
||||
}
|
||||
}
|
||||
return ret.Elements()
|
||||
}
|
||||
|
||||
@@ -390,18 +390,25 @@ type PortPublisher struct {
|
||||
|
||||
// ContainerSummary hold high-level description of a container
|
||||
type ContainerSummary struct {
|
||||
ID string
|
||||
Name string
|
||||
Image any
|
||||
Command string
|
||||
Project string
|
||||
Service string
|
||||
Created int64
|
||||
State string
|
||||
Status string
|
||||
Health string
|
||||
ExitCode int
|
||||
Publishers PortPublishers
|
||||
ID string
|
||||
Name string
|
||||
Names []string
|
||||
Image string
|
||||
Command string
|
||||
Project string
|
||||
Service string
|
||||
Created int64
|
||||
State string
|
||||
Status string
|
||||
Health string
|
||||
ExitCode int
|
||||
Publishers PortPublishers
|
||||
Labels map[string]string
|
||||
SizeRw int64 `json:",omitempty"`
|
||||
SizeRootFs int64 `json:",omitempty"`
|
||||
Mounts []string
|
||||
Networks []string
|
||||
LocalVolumes int
|
||||
}
|
||||
|
||||
// PortPublishers is a slice of PortPublisher
|
||||
|
||||
@@ -98,7 +98,7 @@ func (s *composeService) attachContainer(ctx context.Context, container moby.Con
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, stdin io.ReadCloser, stdout, stderr io.Writer) (func(), chan bool, error) {
|
||||
func (s *composeService) attachContainerStreams(ctx context.Context, container string, tty bool, stdin io.ReadCloser, stdout, stderr io.WriteCloser) (func(), chan bool, error) {
|
||||
detached := make(chan bool)
|
||||
var (
|
||||
restore = func() { /* noop */ }
|
||||
@@ -140,6 +140,8 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container s
|
||||
|
||||
if stdout != nil {
|
||||
go func() {
|
||||
defer stdout.Close() //nolint:errcheck
|
||||
defer stderr.Close() //nolint:errcheck
|
||||
if tty {
|
||||
io.Copy(stdout, streamOut) //nolint:errcheck
|
||||
} else {
|
||||
|
||||
@@ -71,9 +71,9 @@ func TestComposeService_Logs_Demux(t *testing.T) {
|
||||
c1Stdout := stdcopy.NewStdWriter(c1Writer, stdcopy.Stdout)
|
||||
c1Stderr := stdcopy.NewStdWriter(c1Writer, stdcopy.Stderr)
|
||||
go func() {
|
||||
_, err := c1Stdout.Write([]byte("hello\n stdout"))
|
||||
_, err := c1Stdout.Write([]byte("hello stdout\n"))
|
||||
assert.NoError(t, err, "Writing to fake stdout")
|
||||
_, err = c1Stderr.Write([]byte("hello\n stderr"))
|
||||
_, err = c1Stderr.Write([]byte("hello stderr\n"))
|
||||
assert.NoError(t, err, "Writing to fake stderr")
|
||||
_ = c1Writer.Close()
|
||||
}()
|
||||
@@ -94,7 +94,7 @@ func TestComposeService_Logs_Demux(t *testing.T) {
|
||||
|
||||
require.Equal(
|
||||
t,
|
||||
[]string{"hello", " stdout", "hello", " stderr"},
|
||||
[]string{"hello stdout", "hello stderr"},
|
||||
consumer.LogsForContainer("c"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,19 +78,48 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
local int
|
||||
mounts []string
|
||||
)
|
||||
for _, m := range container.Mounts {
|
||||
name := m.Name
|
||||
if name == "" {
|
||||
name = m.Source
|
||||
}
|
||||
if m.Driver == "local" {
|
||||
local++
|
||||
}
|
||||
mounts = append(mounts, name)
|
||||
}
|
||||
|
||||
var networks []string
|
||||
if container.NetworkSettings != nil {
|
||||
for k := range container.NetworkSettings.Networks {
|
||||
networks = append(networks, k)
|
||||
}
|
||||
}
|
||||
|
||||
summary[i] = api.ContainerSummary{
|
||||
ID: container.ID,
|
||||
Name: getCanonicalContainerName(container),
|
||||
Image: container.Image,
|
||||
Project: container.Labels[api.ProjectLabel],
|
||||
Service: container.Labels[api.ServiceLabel],
|
||||
Command: container.Command,
|
||||
State: container.State,
|
||||
Status: container.Status,
|
||||
Created: container.Created,
|
||||
Health: health,
|
||||
ExitCode: exitCode,
|
||||
Publishers: publishers,
|
||||
ID: container.ID,
|
||||
Name: getCanonicalContainerName(container),
|
||||
Names: container.Names,
|
||||
Image: container.Image,
|
||||
Project: container.Labels[api.ProjectLabel],
|
||||
Service: container.Labels[api.ServiceLabel],
|
||||
Command: container.Command,
|
||||
State: container.State,
|
||||
Status: container.Status,
|
||||
Created: container.Created,
|
||||
Labels: container.Labels,
|
||||
SizeRw: container.SizeRw,
|
||||
SizeRootFs: container.SizeRootFs,
|
||||
Mounts: mounts,
|
||||
LocalVolumes: local,
|
||||
Networks: networks,
|
||||
Health: health,
|
||||
ExitCode: exitCode,
|
||||
Publishers: publishers,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -54,13 +54,34 @@ func TestPs(t *testing.T) {
|
||||
containers, err := tested.Ps(ctx, strings.ToLower(testProject), compose.PsOptions{})
|
||||
|
||||
expected := []compose.ContainerSummary{
|
||||
{ID: "123", Name: "123", Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
|
||||
State: "running", Health: "healthy", Publishers: nil},
|
||||
{ID: "456", Name: "456", Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
|
||||
{ID: "123", Name: "123", Names: []string{"/123"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
|
||||
State: "running", Health: "healthy", Publishers: nil,
|
||||
Labels: map[string]string{
|
||||
compose.ProjectLabel: strings.ToLower(testProject),
|
||||
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
|
||||
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
|
||||
compose.ServiceLabel: "service1",
|
||||
},
|
||||
},
|
||||
{ID: "456", Name: "456", Names: []string{"/456"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service1",
|
||||
State: "running", Health: "",
|
||||
Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90, PublishedPort: 80}}},
|
||||
{ID: "789", Name: "789", Image: "foo", Project: strings.ToLower(testProject), Service: "service2",
|
||||
State: "exited", Health: "", ExitCode: 130, Publishers: nil},
|
||||
Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90, PublishedPort: 80}},
|
||||
Labels: map[string]string{
|
||||
compose.ProjectLabel: strings.ToLower(testProject),
|
||||
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
|
||||
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
|
||||
compose.ServiceLabel: "service1",
|
||||
},
|
||||
},
|
||||
{ID: "789", Name: "789", Names: []string{"/789"}, Image: "foo", Project: strings.ToLower(testProject), Service: "service2",
|
||||
State: "exited", Health: "", ExitCode: 130, Publishers: nil,
|
||||
Labels: map[string]string{
|
||||
compose.ProjectLabel: strings.ToLower(testProject),
|
||||
compose.ConfigFilesLabel: "/src/pkg/compose/testdata/compose.yaml",
|
||||
compose.WorkingDirLabel: "/src/pkg/compose/testdata",
|
||||
compose.ServiceLabel: "service2",
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, containers, expected)
|
||||
|
||||
@@ -56,17 +56,48 @@ func (s *composeService) start(ctx context.Context, projectName string, options
|
||||
}
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
// use an independent context tied to the errgroup for background attach operations
|
||||
// the primary context is still used for other operations
|
||||
// this means that once any attach operation fails, all other attaches are cancelled,
|
||||
// but an attach failing won't interfere with the rest of the start
|
||||
eg, attachCtx := errgroup.WithContext(ctx)
|
||||
if listener != nil {
|
||||
attached, err := s.attach(ctx, project, listener, options.AttachTo)
|
||||
_, err := s.attach(attachCtx, project, listener, options.AttachTo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
return s.watchContainers(context.Background(), project.Name, options.AttachTo, options.Services, listener, attached,
|
||||
// it's possible to have a required service whose log output is not desired
|
||||
// (i.e. it's not in the attach set), so watch everything and then filter
|
||||
// calls to attach; this ensures that `watchContainers` blocks until all
|
||||
// required containers have exited, even if their output is not being shown
|
||||
attachTo := utils.NewSet[string](options.AttachTo...)
|
||||
required := utils.NewSet[string](options.Services...)
|
||||
toWatch := attachTo.Union(required).Elements()
|
||||
|
||||
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true, toWatch...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// N.B. this uses the parent context (instead of attachCtx) so that the watch itself can
|
||||
// continue even if one of the log streams fails
|
||||
return s.watchContainers(ctx, project.Name, toWatch, required.Elements(), listener, containers,
|
||||
func(container moby.Container, _ time.Time) error {
|
||||
return s.attachContainer(ctx, container, listener)
|
||||
svc := container.Labels[api.ServiceLabel]
|
||||
if attachTo.Has(svc) {
|
||||
return s.attachContainer(attachCtx, container, listener)
|
||||
}
|
||||
|
||||
// HACK: simulate an "attach" event
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
Container: getContainerNameWithoutProject(container),
|
||||
ID: container.ID,
|
||||
Service: svc,
|
||||
})
|
||||
return nil
|
||||
}, func(container moby.Container, _ time.Time) error {
|
||||
listener(api.ContainerEvent{
|
||||
Type: api.ContainerEventAttach,
|
||||
@@ -156,6 +187,13 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
required = services
|
||||
}
|
||||
|
||||
unexpected := utils.NewSet[string](required...).Diff(utils.NewSet[string](services...))
|
||||
if len(unexpected) != 0 {
|
||||
return fmt.Errorf(`required service(s) "%s" not present in watched service(s) "%s"`,
|
||||
strings.Join(unexpected.Elements(), ", "),
|
||||
strings.Join(services, ", "))
|
||||
}
|
||||
|
||||
// predicate to tell if a container we receive event for should be considered or ignored
|
||||
ofInterest := func(c moby.Container) bool {
|
||||
if len(services) > 0 {
|
||||
@@ -190,6 +228,12 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
err := s.Events(ctx, projectName, api.EventsOptions{
|
||||
Services: services,
|
||||
Consumer: func(event api.Event) error {
|
||||
defer func() {
|
||||
// after consuming each event, check to see if we're done
|
||||
if len(expected) == 0 {
|
||||
stop()
|
||||
}
|
||||
}()
|
||||
inspected, err := s.apiClient().ContainerInspect(ctx, event.Container)
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
@@ -291,9 +335,6 @@ func (s *composeService) watchContainers(ctx context.Context, //nolint:gocyclo
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(expected) == 0 {
|
||||
stop()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
@@ -29,18 +29,16 @@ import (
|
||||
func RequireServiceState(t testing.TB, cli *CLI, service string, state string) {
|
||||
t.Helper()
|
||||
psRes := cli.RunDockerComposeCmd(t, "ps", "--format=json", service)
|
||||
var psOut []map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal([]byte(psRes.Stdout()), &psOut),
|
||||
var svc map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal([]byte(psRes.Stdout()), &svc),
|
||||
"Invalid `compose ps` JSON output")
|
||||
|
||||
for _, svc := range psOut {
|
||||
require.Equal(t, service, svc["Service"],
|
||||
"Found ps output for unexpected service")
|
||||
require.Equalf(t,
|
||||
strings.ToLower(state),
|
||||
strings.ToLower(svc["State"].(string)),
|
||||
"Service %q (%s) not in expected state",
|
||||
service, svc["Name"],
|
||||
)
|
||||
}
|
||||
require.Equal(t, service, svc["Service"],
|
||||
"Found ps output for unexpected service")
|
||||
require.Equalf(t,
|
||||
strings.ToLower(state),
|
||||
strings.ToLower(svc["State"].(string)),
|
||||
"Service %q (%s) not in expected state",
|
||||
service, svc["Name"],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,10 +71,14 @@ func TestPortRange(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
const projectName = "e2e-port-range"
|
||||
|
||||
reset := func() {
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans", "--timeout=0")
|
||||
}
|
||||
reset()
|
||||
t.Cleanup(reset)
|
||||
|
||||
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/port-range/compose.yaml", "--project-name", projectName, "up", "-d")
|
||||
res.Assert(t, icmd.Success)
|
||||
|
||||
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--remove-orphans")
|
||||
}
|
||||
|
||||
func TestStdoutStderr(t *testing.T) {
|
||||
|
||||
@@ -361,13 +361,14 @@ func IsHealthy(service string) func(res *icmd.Result) bool {
|
||||
Health string `json:"health"`
|
||||
}
|
||||
|
||||
ps := []state{}
|
||||
err := json.Unmarshal([]byte(res.Stdout()), &ps)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, state := range ps {
|
||||
if state.Name == service && state.Health == "healthy" {
|
||||
decoder := json.NewDecoder(strings.NewReader(res.Stdout()))
|
||||
for decoder.More() {
|
||||
ps := state{}
|
||||
err := decoder.Decode(&ps)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if ps.Name == service && ps.Health == "healthy" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,16 +138,14 @@ func urlForService(t testing.TB, cli *CLI, service string, targetPort int) strin
|
||||
func publishedPortForService(t testing.TB, cli *CLI, service string, targetPort int) int {
|
||||
t.Helper()
|
||||
res := cli.RunDockerComposeCmd(t, "ps", "--format=json", service)
|
||||
var psOut []struct {
|
||||
var svc struct {
|
||||
Publishers []struct {
|
||||
TargetPort int
|
||||
PublishedPort int
|
||||
}
|
||||
}
|
||||
require.NoError(t, json.Unmarshal([]byte(res.Stdout()), &psOut),
|
||||
require.NoError(t, json.Unmarshal([]byte(res.Stdout()), &svc),
|
||||
"Failed to parse `%s` output", res.Cmd.String())
|
||||
require.Len(t, psOut, 1, "Expected exactly 1 service")
|
||||
svc := psOut[0]
|
||||
for _, pp := range svc.Publishers {
|
||||
if pp.TargetPort == targetPort {
|
||||
return pp.PublishedPort
|
||||
|
||||
@@ -31,7 +31,7 @@ const (
|
||||
|
||||
func TestExplicitProfileUsage(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
const projectName = "compose-e2e-profiles"
|
||||
const projectName = "compose-e2e-explicit-profiles"
|
||||
const profileName = "test-profile"
|
||||
|
||||
t.Run("compose up with profile", func(t *testing.T) {
|
||||
@@ -132,7 +132,7 @@ func TestNoProfileUsage(t *testing.T) {
|
||||
|
||||
func TestActiveProfileViaTargetedService(t *testing.T) {
|
||||
c := NewParallelCLI(t)
|
||||
const projectName = "compose-e2e-profiles-via-target-service"
|
||||
const projectName = "compose-e2e-via-target-service-profiles"
|
||||
const profileName = "test-profile"
|
||||
|
||||
t.Run("compose up with service name", func(t *testing.T) {
|
||||
|
||||
@@ -62,9 +62,18 @@ func TestPs(t *testing.T) {
|
||||
t.Run("json", func(t *testing.T) {
|
||||
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps",
|
||||
"--format", "json")
|
||||
var output []api.ContainerSummary
|
||||
err := json.Unmarshal([]byte(res.Stdout()), &output)
|
||||
require.NoError(t, err, "Failed to unmarshal ps JSON output")
|
||||
type element struct {
|
||||
Name string
|
||||
Publishers api.PortPublishers
|
||||
}
|
||||
var output []element
|
||||
out := res.Stdout()
|
||||
dec := json.NewDecoder(strings.NewReader(out))
|
||||
for dec.More() {
|
||||
var s element
|
||||
require.NoError(t, dec.Decode(&s), "Failed to unmarshal ps JSON output")
|
||||
output = append(output, s)
|
||||
}
|
||||
|
||||
count := 0
|
||||
assert.Equal(t, 2, len(output))
|
||||
|
||||
@@ -71,6 +71,9 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
|
||||
CopyFile(t, filepath.Join("fixtures", "watch", "compose.yaml"), composeFilePath)
|
||||
|
||||
projName := "e2e-watch-" + svcName
|
||||
if tarSync {
|
||||
projName += "-tar"
|
||||
}
|
||||
env := []string{
|
||||
"COMPOSE_FILE=" + composeFilePath,
|
||||
"COMPOSE_PROJECT_NAME=" + projName,
|
||||
@@ -96,6 +99,7 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
|
||||
t.Cleanup(func() {
|
||||
// IMPORTANT: watch doesn't exit on its own, don't leak processes!
|
||||
if r.Cmd.Process != nil {
|
||||
t.Logf("Killing watch process: pid[%d]", r.Cmd.Process.Pid)
|
||||
_ = r.Cmd.Process.Kill()
|
||||
}
|
||||
})
|
||||
|
||||
192
pkg/remote/git.go
Normal file
192
pkg/remote/git.go
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
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 remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/loader"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/moby/buildkit/util/gitutil"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func GitRemoteLoaderEnabled() (bool, error) {
|
||||
if v := os.Getenv("COMPOSE_EXPERIMENTAL_GIT_REMOTE"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "COMPOSE_EXPERIMENTAL_GIT_REMOTE environment variable expects boolean value")
|
||||
}
|
||||
return enabled, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func NewGitRemoteLoader() (loader.ResourceLoader, error) {
|
||||
// xdg.CacheFile creates the parent directories for the target file path
|
||||
// and returns the fully qualified path, so use "git" as a filename and
|
||||
// then chop it off after, i.e. no ~/.cache/docker-compose/git file will
|
||||
// ever be created
|
||||
cache, err := xdg.CacheFile(filepath.Join("docker-compose", "git"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initializing git cache: %w", err)
|
||||
}
|
||||
cache = filepath.Dir(cache)
|
||||
return gitRemoteLoader{
|
||||
cache: cache,
|
||||
}, err
|
||||
}
|
||||
|
||||
type gitRemoteLoader struct {
|
||||
cache string
|
||||
}
|
||||
|
||||
func (g gitRemoteLoader) Accept(path string) bool {
|
||||
_, err := gitutil.ParseGitRef(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
var commitSHA = regexp.MustCompile(`^[a-f0-9]{40}$`)
|
||||
|
||||
func (g gitRemoteLoader) Load(ctx context.Context, path string) (string, error) {
|
||||
ref, err := gitutil.ParseGitRef(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ref.Commit == "" {
|
||||
ref.Commit = "HEAD" // default branch
|
||||
}
|
||||
|
||||
if !commitSHA.MatchString(ref.Commit) {
|
||||
cmd := exec.CommandContext(ctx, "git", "ls-remote", "--exit-code", ref.Remote, ref.Commit)
|
||||
cmd.Env = g.gitCommandEnv()
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
if cmd.ProcessState.ExitCode() == 2 {
|
||||
return "", errors.Wrapf(err, "repository does not contain ref %s, output: %q", path, string(out))
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if len(out) < 40 {
|
||||
return "", fmt.Errorf("unexpected git command output: %q", string(out))
|
||||
}
|
||||
sha := string(out[:40])
|
||||
if !commitSHA.MatchString(sha) {
|
||||
return "", fmt.Errorf("invalid commit sha %q", sha)
|
||||
}
|
||||
ref.Commit = sha
|
||||
}
|
||||
|
||||
local := filepath.Join(g.cache, ref.Commit)
|
||||
if _, err := os.Stat(local); os.IsNotExist(err) {
|
||||
err = g.checkout(ctx, local, ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if ref.SubDir != "" {
|
||||
local = filepath.Join(local, ref.SubDir)
|
||||
}
|
||||
stat, err := os.Stat(local)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if stat.IsDir() {
|
||||
local, err = findFile(cli.DefaultFileNames, local)
|
||||
}
|
||||
return local, err
|
||||
}
|
||||
|
||||
func (g gitRemoteLoader) checkout(ctx context.Context, path string, ref *gitutil.GitRef) error {
|
||||
err := os.MkdirAll(path, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = exec.CommandContext(ctx, "git", "init", path).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "git", "remote", "add", "origin", ref.Remote)
|
||||
cmd.Dir = path
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "fetch", "--depth=1", "origin", ref.Commit)
|
||||
cmd.Env = g.gitCommandEnv()
|
||||
cmd.Dir = path
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "git", "checkout", ref.Commit)
|
||||
cmd.Dir = path
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g gitRemoteLoader) gitCommandEnv() []string {
|
||||
env := types.NewMapping(os.Environ())
|
||||
if env["GIT_TERMINAL_PROMPT"] == "" {
|
||||
// Disable prompting for passwords by Git until user explicitly asks for it.
|
||||
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||
}
|
||||
if env["GIT_SSH"] == "" && env["GIT_SSH_COMMAND"] == "" {
|
||||
// Disable any ssh connection pooling by Git and do not attempt to prompt the user.
|
||||
env["GIT_SSH_COMMAND"] = "ssh -o ControlMaster=no -o BatchMode=yes"
|
||||
}
|
||||
v := values(env)
|
||||
return v
|
||||
}
|
||||
|
||||
func findFile(names []string, pwd string) (string, error) {
|
||||
for _, n := range names {
|
||||
f := filepath.Join(pwd, n)
|
||||
if fi, err := os.Stat(f); err == nil && !fi.IsDir() {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return "", api.ErrNotFound
|
||||
}
|
||||
|
||||
var _ loader.ResourceLoader = gitRemoteLoader{}
|
||||
|
||||
func values(m types.Mapping) []string {
|
||||
values := make([]string, 0, len(m))
|
||||
for k, v := range m {
|
||||
values = append(values, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -16,6 +16,23 @@ package utils
|
||||
|
||||
type Set[T comparable] map[T]struct{}
|
||||
|
||||
func NewSet[T comparable](v ...T) Set[T] {
|
||||
if len(v) == 0 {
|
||||
return make(Set[T])
|
||||
}
|
||||
|
||||
out := make(Set[T], len(v))
|
||||
for i := range v {
|
||||
out.Add(v[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s Set[T]) Has(v T) bool {
|
||||
_, ok := s[v]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s Set[T]) Add(v T) {
|
||||
s[v] = struct{}{}
|
||||
}
|
||||
@@ -53,3 +70,24 @@ func (s Set[T]) RemoveAll(elements ...T) {
|
||||
s.Remove(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Set[T]) Diff(other Set[T]) Set[T] {
|
||||
out := make(Set[T])
|
||||
for k := range s {
|
||||
if _, ok := other[k]; !ok {
|
||||
out[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s Set[T]) Union(other Set[T]) Set[T] {
|
||||
out := make(Set[T])
|
||||
for k := range s {
|
||||
out[k] = struct{}{}
|
||||
}
|
||||
for k := range other {
|
||||
out[k] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
41
pkg/utils/set_test.go
Normal file
41
pkg/utils/set_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2022 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 utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSet_Has(t *testing.T) {
|
||||
x := NewSet[string]("value")
|
||||
require.True(t, x.Has("value"))
|
||||
require.False(t, x.Has("VALUE"))
|
||||
}
|
||||
|
||||
func TestSet_Diff(t *testing.T) {
|
||||
a := NewSet[int](1, 2)
|
||||
b := NewSet[int](2, 3)
|
||||
require.ElementsMatch(t, []int{1}, a.Diff(b).Elements())
|
||||
require.ElementsMatch(t, []int{3}, b.Diff(a).Elements())
|
||||
}
|
||||
|
||||
func TestSet_Union(t *testing.T) {
|
||||
a := NewSet[int](1, 2)
|
||||
b := NewSet[int](2, 3)
|
||||
require.ElementsMatch(t, []int{1, 2, 3}, a.Union(b).Elements())
|
||||
require.ElementsMatch(t, []int{1, 2, 3}, b.Union(a).Elements())
|
||||
}
|
||||
@@ -43,17 +43,11 @@ func (s *splitWriter) Write(b []byte) (int, error) {
|
||||
for {
|
||||
b = s.buffer.Bytes()
|
||||
index := bytes.Index(b, []byte{'\n'})
|
||||
if index > 0 {
|
||||
line := s.buffer.Next(index + 1)
|
||||
s.consumer(string(line[:len(line)-1]))
|
||||
} else {
|
||||
line := s.buffer.String()
|
||||
s.buffer.Reset()
|
||||
if len(line) > 0 {
|
||||
s.consumer(line)
|
||||
}
|
||||
if index < 0 {
|
||||
break
|
||||
}
|
||||
line := s.buffer.Next(index + 1)
|
||||
s.consumer(string(line[:len(line)-1]))
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -28,21 +28,13 @@ func TestSplitWriter(t *testing.T) {
|
||||
w := GetWriter(func(line string) {
|
||||
lines = append(lines, line)
|
||||
})
|
||||
w.Write([]byte("hello\n"))
|
||||
w.Write([]byte("world\n"))
|
||||
w.Write([]byte("!"))
|
||||
assert.DeepEqual(t, lines, []string{"hello", "world", "!"})
|
||||
|
||||
}
|
||||
|
||||
//nolint:errcheck
|
||||
func TestSplitWriterNoEOL(t *testing.T) {
|
||||
var lines []string
|
||||
w := GetWriter(func(line string) {
|
||||
lines = append(lines, line)
|
||||
})
|
||||
w.Write([]byte("hello\n"))
|
||||
w.Write([]byte("world!"))
|
||||
w.Write([]byte("h"))
|
||||
w.Write([]byte("e"))
|
||||
w.Write([]byte("l"))
|
||||
w.Write([]byte("l"))
|
||||
w.Write([]byte("o"))
|
||||
w.Write([]byte("\n"))
|
||||
w.Write([]byte("world!\n"))
|
||||
assert.DeepEqual(t, lines, []string{"hello", "world!"})
|
||||
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
|
||||
"github.com/moby/patternmatcher"
|
||||
"github.com/moby/patternmatcher/ignorefile"
|
||||
)
|
||||
|
||||
type dockerPathMatcher struct {
|
||||
@@ -133,13 +133,17 @@ func readDockerignorePatterns(repoRoot string) ([]string, error) {
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
return dockerignore.ReadAll(f)
|
||||
patterns, err := ignorefile.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading .dockerignore: %w", err)
|
||||
}
|
||||
return patterns, nil
|
||||
}
|
||||
|
||||
func DockerIgnoreTesterFromContents(repoRoot string, contents string) (*dockerPathMatcher, error) {
|
||||
patterns, err := dockerignore.ReadAll(strings.NewReader(contents))
|
||||
patterns, err := ignorefile.ReadAll(strings.NewReader(contents))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error reading .dockerignore: %w", err)
|
||||
}
|
||||
|
||||
return NewDockerPatternMatcher(repoRoot, patterns)
|
||||
|
||||
Reference in New Issue
Block a user