Compare commits

...

16 Commits

Author SHA1 Message Date
Milas Bowman
d6f842b042 test: e2e test reliability improvements (#10950)
* Use unique project name prefixes (some of these tests assert
  on output using the project name as a magic string, so could
  be impacted by other tests with the same project name prefix)
* Tear down port range project before starting to try and avoid
  race conditions with the engine and port assignment

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 16:01:15 -04:00
Milas Bowman
4fbbf201cd build(deps): upgrade to compose-go v1.18.3 (#10947)
https://github.com/compose-spec/compose-go/releases/tag/v1.18.3

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-30 09:17:57 -04:00
Bilal Khan
935d72f46f added the dot at the end of the sentence
Signed-off-by: Bilal Khan <bilalkhanrecovered@gmail.com>
2023-08-28 09:19:26 +02:00
Nicolas De Loof
41682acc77 add support for attributes exposed by docker ps
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-25 16:36:45 +02:00
Nicolas De Loof
1054792b47 align docker compose ps with docker CLI to support --format
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-25 16:36:45 +02:00
Milas Bowman
19f66918cc watch: only allow a single instance per-project
This is a good place to start introducing (local) exclusivity
to Compose. Now, when `alpha watch` launches, it will check for
the existence of a PID file in the user XDG runtime directory,
and create one if the existing one is stale or does not exist.
If the PID file exists and is valid, an error is returned and
Compose exits.

A slight tweak to the experimental remote Git loader has been
made to use the XDG package for consistency.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-25 15:49:28 +02:00
Milas Bowman
186744e034 ci: bump golangci-lint to v1.54.2
Also improve incremental lint caching.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-24 08:57:47 +02:00
Milas Bowman
bc9d696fa0 Merge pull request #10922 from thaJeztah/replace_dockerignore
replace dockerfile/dockerignore with patternmatcher/ignorefile
2023-08-23 16:04:19 -04:00
Nicolas De loof
6204fb1c94 logs: fix for missing output on container exit (#10925)
We can't assume we receive container logs line by line. Some framework won't buffer output and will send char by char, and we also can receive looong lines which get buffered to 32kb and then cut into multiple logs.

This assumes we will catch container streams being closed before we receive a die event for container, which could be subject to race condition, but at least the impact here is minimal and the fix works for reproduction examples provided in linked issues.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-23 08:57:18 -04:00
Sebastiaan van Stijn
5d732010a7 replace dockerfile/dockerignore with patternmatcher/ignorefile
The BuildKit dockerignore package was integrated in the patternmatcher
repository / module. This patch updates our uses of the BuildKit package
with its new location.

A small local change was made to keep the format of the existing error message,
because the "ignorefile" package is slightly more agnostic in that respect
and doesn't include ".dockerignore" in the error message.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-08-23 00:43:17 +02:00
Sebastiaan van Stijn
2006f3fe7d go.mod: github.com/moby/patternmatcher v0.6.0
- integrate frontend/dockerfile/dockerignore from buildkit

full diff: https://github.com/moby/patternmatcher/compare/v0.5.0...v0.6.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-08-23 00:40:12 +02:00
Sebastiaan van Stijn
192718c001 go.mod: remove some outdated comments
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-08-23 00:39:35 +02:00
Milas Bowman
c79f67fead otel: add include to project up span
Flatten the list of included files and add as a slice attribute.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-22 16:10:18 +02:00
dependabot[bot]
3b294bfdda build(deps): bump github.com/compose-spec/compose-go from 1.18.1 to 1.18.2 (#10915)
build(deps): bump github.com/compose-spec/compose-go

Bumps [github.com/compose-spec/compose-go](https://github.com/compose-spec/compose-go) from 1.18.1 to 1.18.2.
- [Release notes](https://github.com/compose-spec/compose-go/releases)
- [Commits](https://github.com/compose-spec/compose-go/compare/v1.18.1...v1.18.2)

---
updated-dependencies:
- dependency-name: github.com/compose-spec/compose-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 14:02:03 -04:00
Nicolas De loof
dd34f7a22b include: add experimental support for Git resources (#10811)
Requires setting `COMPOSE_EXPERIMENTAL_GIT_REMOTE=1`.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2023-08-18 09:16:45 -04:00
Milas Bowman
caad72713b up: handle various attach use cases better
By default, `compose up` attaches to all services (i.e.
shows log output from every associated container). If
a service is specified, e.g. `compose up foo`, then
only `foo`'s logs are tailed. The `--attach-dependencies`
flag can also be used, so that if `foo` depended upon
`bar`, then `bar`'s logs would also be followed. It's
also possible to use `--no-attach` to filter out one
or more services explicitly, e.g. `compose up --no-attach=noisy`
would launch all services, including `noisy`, and would
show log output from every service _except_ `noisy`.
Lastly, it's possible to use `up --attach` to explicitly
restrict to a subset of services (or their dependencies).

How these flags interact with each other is also worth
thinking through.

There were a few different connected issues here, but
the primary issue was that running `compose up foo` was
always attaching dependencies regardless of `--attach-dependencies`.

The filtering logic here has been updated so that it
behaves predictably both when launching all services
(`compose up`) or a subset (`compose up foo`) as well
as various flag combinations on top of those.

Notably, this required making some changes to how it
watches containers. The logic here between attaching
for logs and monitoring for lifecycle changes is
tightly coupled, so some changes were needed to ensure
that the full set of services being `up`'d are _watched_
and the subset that should have logs shown are _attached_.
(This does mean faking the attach with an event but not
actually doing it.)

While handling that, I adjusted the context lifetimes
here, which improves error handling that gets shown to
the user and should help avoid potential leaks by getting
rid of a `context.Background()`.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
2023-08-18 12:38:38 +02:00
36 changed files with 998 additions and 272 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View 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())
}

View File

@@ -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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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())
}

View File

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

View File

@@ -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!"})
}

View File

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