move progress UI components into cmd

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2025-11-05 17:08:53 +01:00 committed by Guillaume Lours
parent 5ef495c898
commit aff5c115d6
45 changed files with 619 additions and 596 deletions

View File

@ -26,8 +26,8 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
cliopts "github.com/docker/cli/opts" cliopts "github.com/docker/cli/opts"
"github.com/docker/compose/v2/cmd/display"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
@ -67,8 +67,8 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
builderName = os.Getenv("BUILDX_BUILDER") builderName = os.Getenv("BUILDX_BUILDER")
} }
uiMode := ui.Mode uiMode := display.Mode
if uiMode == ui.ModeJSON { if uiMode == display.ModeJSON {
uiMode = "rawjson" uiMode = "rawjson"
} }
@ -100,7 +100,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back
Short: "Build or rebuild services", Short: "Build or rebuild services",
PreRunE: Adapt(func(ctx context.Context, args []string) error { PreRunE: Adapt(func(ctx context.Context, args []string) error {
if opts.quiet { if opts.quiet {
ui.Mode = ui.ModeQuiet display.Mode = display.ModeQuiet
devnull, err := os.Open(os.DevNull) devnull, err := os.Open(os.DevNull)
if err != nil { if err != nil {
return err return err
@ -151,7 +151,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back
func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts buildOptions, services []string) error { func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts buildOptions, services []string) error {
if opts.print { if opts.print {
backendOptions.Add(compose.WithEventProcessor(ui.NewQuietWriter())) backendOptions.Add(compose.WithEventProcessor(display.Quiet()))
} }
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil { if err != nil {

View File

@ -39,11 +39,11 @@ import (
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/pkg/kvfile" "github.com/docker/cli/pkg/kvfile"
"github.com/docker/compose/v2/cmd/display"
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
ui "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/remote" "github.com/docker/compose/v2/pkg/remote"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
"github.com/morikuni/aec" "github.com/morikuni/aec"
@ -84,10 +84,16 @@ func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(ke
return nil return nil
} }
var stdioToStdout bool
func init() { func init() {
// compose evaluates env file values for interpolation // compose evaluates env file values for interpolation
// `raw` format allows to load env_file with the same parser used by docker run --env-file // `raw` format allows to load env_file with the same parser used by docker run --env-file
dotenv.RegisterFormat("raw", rawEnv) dotenv.RegisterFormat("raw", rawEnv)
if v, ok := os.LookupEnv("COMPOSE_STATUS_STDOUT"); ok {
stdioToStdout, _ = strconv.ParseBool(v)
}
} }
// Command defines a compose CLI command as a func with args // Command defines a compose CLI command as a func with args
@ -116,7 +122,7 @@ func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error {
StatusCode: 130, StatusCode: 130,
} }
} }
if ui.Mode == ui.ModeJSON { if display.Mode == display.ModeJSON {
err = makeJSONError(err) err = makeJSONError(err)
} }
return err return err
@ -486,49 +492,49 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C
formatter.SetANSIMode(dockerCli, ansi) formatter.SetANSIMode(dockerCli, ansi)
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" { if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
ui.NoColor() display.NoColor()
formatter.SetANSIMode(dockerCli, formatter.Never) formatter.SetANSIMode(dockerCli, formatter.Never)
} }
switch ansi { switch ansi {
case "never": case "never":
ui.Mode = ui.ModePlain display.Mode = display.ModePlain
case "always": case "always":
ui.Mode = ui.ModeTTY display.Mode = display.ModeTTY
} }
var ep ui.EventProcessor var ep api.EventProcessor
switch opts.Progress { switch opts.Progress {
case "", ui.ModeAuto: case "", display.ModeAuto:
switch { switch {
case ansi == "never": case ansi == "never":
ui.Mode = ui.ModePlain display.Mode = display.ModePlain
ep = ui.NewPlainWriter(dockerCli.Err()) ep = display.Plain(dockerCli.Err())
case dockerCli.Out().IsTerminal(): case dockerCli.Out().IsTerminal():
ep = ui.NewTTYWriter(dockerCli.Err()) ep = display.Full(dockerCli.Err(), stdinfo(dockerCli))
default: default:
ep = ui.NewPlainWriter(dockerCli.Err()) ep = display.Plain(dockerCli.Err())
} }
case ui.ModeTTY: case display.ModeTTY:
if ansi == "never" { if ansi == "never" {
return fmt.Errorf("can't use --progress tty while ANSI support is disabled") return fmt.Errorf("can't use --progress tty while ANSI support is disabled")
} }
ui.Mode = ui.ModeTTY display.Mode = display.ModeTTY
ep = ui.NewTTYWriter(dockerCli.Err()) ep = display.Full(dockerCli.Err(), stdinfo(dockerCli))
case ui.ModePlain: case display.ModePlain:
if ansi == "always" { if ansi == "always" {
return fmt.Errorf("can't use --progress plain while ANSI support is forced") return fmt.Errorf("can't use --progress plain while ANSI support is forced")
} }
ui.Mode = ui.ModePlain display.Mode = display.ModePlain
ep = ui.NewPlainWriter(dockerCli.Err()) ep = display.Plain(dockerCli.Err())
case ui.ModeQuiet, "none": case display.ModeQuiet, "none":
ui.Mode = ui.ModeQuiet display.Mode = display.ModeQuiet
ep = ui.NewQuietWriter() ep = display.Quiet()
case ui.ModeJSON: case display.ModeJSON:
ui.Mode = ui.ModeJSON display.Mode = display.ModeJSON
logrus.SetFormatter(&logrus.JSONFormatter{}) logrus.SetFormatter(&logrus.JSONFormatter{})
ep = ui.NewJSONWriter(dockerCli.Err()) ep = display.JSON(dockerCli.Err())
default: default:
return fmt.Errorf("unsupported --progress value %q", opts.Progress) return fmt.Errorf("unsupported --progress value %q", opts.Progress)
} }
@ -658,6 +664,13 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C
return c return c
} }
func stdinfo(dockerCli command.Cli) io.Writer {
if stdioToStdout {
return dockerCli.Out()
}
return dockerCli.Err()
}
func setEnvWithDotEnv(opts ProjectOptions) error { func setEnvWithDotEnv(opts ProjectOptions) error {
options, err := cli.NewProjectOptions(opts.ConfigPaths, options, err := cli.NewProjectOptions(opts.ConfigPaths,
cli.WithWorkingDirectory(opts.ProjectDir), cli.WithWorkingDirectory(opts.ProjectDir),
@ -683,9 +696,9 @@ func setEnvWithDotEnv(opts ProjectOptions) error {
} }
var printerModes = []string{ var printerModes = []string{
ui.ModeAuto, display.ModeAuto,
ui.ModeTTY, display.ModeTTY,
ui.ModePlain, display.ModePlain,
ui.ModeJSON, display.ModeJSON,
ui.ModeQuiet, display.ModeQuiet,
} }

View File

@ -18,6 +18,8 @@ package compose
import ( import (
"context" "context"
"errors"
"fmt"
"os" "os"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
@ -65,10 +67,15 @@ func runKill(ctx context.Context, dockerCli command.Cli, backendOptions *Backend
if err != nil { if err != nil {
return err return err
} }
return backend.Kill(ctx, name, api.KillOptions{ err = backend.Kill(ctx, name, api.KillOptions{
RemoveOrphans: opts.removeOrphans, RemoveOrphans: opts.removeOrphans,
Project: project, Project: project,
Services: services, Services: services,
Signal: opts.signal, Signal: opts.signal,
}) })
if errors.Is(err, api.ErrNoResources) {
_, _ = fmt.Fprintln(stdinfo(dockerCli), "No container to kill")
return nil
}
return err
} }

View File

@ -22,7 +22,6 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/cmd/formatter"
@ -107,28 +106,28 @@ func runLogs(ctx context.Context, dockerCli command.Cli, backendOptions *Backend
var _ api.LogConsumer = &logConsumer{} var _ api.LogConsumer = &logConsumer{}
type logConsumer struct { type logConsumer struct {
events progress.EventProcessor events api.EventProcessor
} }
func (l logConsumer) Log(containerName, message string) { func (l logConsumer) Log(containerName, message string) {
l.events.On(progress.Event{ l.events.On(api.Resource{
ID: containerName, ID: containerName,
Text: message, Text: message,
}) })
} }
func (l logConsumer) Err(containerName, message string) { func (l logConsumer) Err(containerName, message string) {
l.events.On(progress.Event{ l.events.On(api.Resource{
ID: containerName, ID: containerName,
Status: progress.Error, Status: api.Error,
Text: message, Text: message,
}) })
} }
func (l logConsumer) Status(containerName, message string) { func (l logConsumer) Status(containerName, message string) {
l.events.On(progress.Event{ l.events.On(api.Resource{
ID: containerName, ID: containerName,
Status: progress.Error, Status: api.Error,
Text: message, Text: message,
}) })
} }

View File

@ -30,9 +30,9 @@ import (
"github.com/compose-spec/compose-go/v2/template" "github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/display"
"github.com/docker/compose/v2/cmd/prompt" "github.com/docker/compose/v2/cmd/prompt"
"github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/internal/tracing"
ui "github.com/docker/compose/v2/pkg/progress"
) )
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error { func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
@ -247,7 +247,7 @@ func displayInterpolationVariables(writer io.Writer, varsInfo []varInfo) {
func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, options buildOptions) { func displayLocationRemoteStack(dockerCli command.Cli, project *types.Project, options buildOptions) {
mainComposeFile := options.ProjectOptions.ConfigPaths[0] //nolint:staticcheck mainComposeFile := options.ProjectOptions.ConfigPaths[0] //nolint:staticcheck
if ui.Mode != ui.ModeQuiet && ui.Mode != ui.ModeJSON { if display.Mode != display.ModeQuiet && display.Mode != display.ModeJSON {
_, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir) _, _ = fmt.Fprintf(dockerCli.Out(), "Your compose stack %q is stored in %q\n", mainComposeFile, project.WorkingDir)
} }
} }

View File

@ -18,6 +18,8 @@ package compose
import ( import (
"context" "context"
"errors"
"fmt"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
@ -70,11 +72,16 @@ func runRemove(ctx context.Context, dockerCli command.Cli, backendOptions *Backe
if err != nil { if err != nil {
return err return err
} }
return backend.Remove(ctx, name, api.RemoveOptions{ err = backend.Remove(ctx, name, api.RemoveOptions{
Services: services, Services: services,
Force: opts.force, Force: opts.force,
Volumes: opts.volumes, Volumes: opts.volumes,
Project: project, Project: project,
Stop: opts.stop, Stop: opts.stop,
}) })
if errors.Is(err, api.ErrNoResources) {
_, _ = fmt.Fprintln(stdinfo(dockerCli), "No stopped containers")
return nil
}
return err
} }

View File

@ -25,8 +25,8 @@ import (
composecli "github.com/compose-spec/compose-go/v2/cli" composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/format" "github.com/compose-spec/compose-go/v2/format"
"github.com/docker/compose/v2/cmd/display"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
"github.com/docker/compose/v2/pkg/progress"
xprogress "github.com/moby/buildkit/util/progress/progressui" xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -193,7 +193,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
} }
if options.quiet { if options.quiet {
progress.Mode = progress.ModeQuiet display.Mode = display.ModeQuiet
devnull, err := os.Open(os.DevNull) devnull, err := os.Open(os.DevNull)
if err != nil { if err != nil {
return err return err

View File

@ -26,8 +26,8 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/display"
"github.com/docker/compose/v2/pkg/compose" "github.com/docker/compose/v2/pkg/compose"
ui "github.com/docker/compose/v2/pkg/progress"
xprogress "github.com/moby/buildkit/util/progress/progressui" xprogress "github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -341,7 +341,7 @@ func runUp(
WaitTimeout: timeout, WaitTimeout: timeout,
Watch: upOptions.watch, Watch: upOptions.watch,
Services: services, Services: services,
NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain" && dockerCli.In().IsTerminal(), NavigationMenu: upOptions.navigationMenu && display.Mode != "plain" && dockerCli.In().IsTerminal(),
}, },
}) })
} }

View File

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
package progress package display
import ( import (
"github.com/morikuni/aec" "github.com/morikuni/aec"

View File

@ -14,16 +14,18 @@
limitations under the License. limitations under the License.
*/ */
package progress package display
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"github.com/docker/compose/v2/pkg/api"
) )
func NewJSONWriter(out io.Writer) EventProcessor { func JSON(out io.Writer) api.EventProcessor {
return &jsonWriter{ return &jsonWriter{
out: out, out: out,
} }
@ -50,7 +52,7 @@ type jsonMessage struct {
func (p *jsonWriter) Start(ctx context.Context, operation string) { func (p *jsonWriter) Start(ctx context.Context, operation string) {
} }
func (p *jsonWriter) Event(e Event) { func (p *jsonWriter) Event(e api.Resource) {
message := &jsonMessage{ message := &jsonMessage{
DryRun: p.dryRun, DryRun: p.dryRun,
Tail: false, Tail: false,
@ -69,7 +71,7 @@ func (p *jsonWriter) Event(e Event) {
} }
} }
func (p *jsonWriter) On(events ...Event) { func (p *jsonWriter) On(events ...api.Resource) {
for _, e := range events { for _, e := range events {
p.Event(e) p.Event(e)
} }

View File

@ -14,13 +14,14 @@
limitations under the License. limitations under the License.
*/ */
package progress package display
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"testing" "testing"
"github.com/docker/compose/v2/pkg/api"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@ -31,11 +32,11 @@ func TestJsonWriter_Event(t *testing.T) {
dryRun: true, dryRun: true,
} }
event := Event{ event := api.Resource{
ID: "service1", ID: "service1",
ParentID: "project", ParentID: "project",
Status: Working, Status: api.Working,
Text: StatusCreating, Text: api.StatusCreating,
Current: 50, Current: 50,
Total: 100, Total: 100,
Percent: 50, Percent: 50,
@ -50,7 +51,7 @@ func TestJsonWriter_Event(t *testing.T) {
DryRun: true, DryRun: true,
ID: event.ID, ID: event.ID,
ParentID: event.ParentID, ParentID: event.ParentID,
Text: StatusCreating, Text: api.StatusCreating,
Status: "Working", Status: "Working",
Current: event.Current, Current: event.Current,
Total: event.Total, Total: event.Total,

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 Docker Compose CLI authors Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,20 +14,10 @@
limitations under the License. limitations under the License.
*/ */
package progress package display
import ( // Mode define how progress should be rendered, either as ModePlain or ModeTTY
"context" var Mode = ModeAuto
)
type progressFunc func(context.Context) error
func Run(ctx context.Context, pf progressFunc, operation string, bus EventProcessor) error {
bus.Start(ctx, operation)
err := pf(ctx)
bus.Done(operation, err != nil)
return err
}
const ( const (
// ModeAuto detect console capabilities // ModeAuto detect console capabilities
@ -41,6 +31,3 @@ const (
// ModeJSON outputs a machine-readable JSON stream // ModeJSON outputs a machine-readable JSON stream
ModeJSON = "json" ModeJSON = "json"
) )
// Mode define how progress should be rendered, either as ModePlain or ModeTTY
var Mode = ModeAuto

View File

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
package progress package display
import ( import (
"context" "context"
@ -24,7 +24,7 @@ import (
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
) )
func NewPlainWriter(out io.Writer) EventProcessor { func Plain(out io.Writer) api.EventProcessor {
return &plainWriter{ return &plainWriter{
out: out, out: out,
} }
@ -38,7 +38,7 @@ type plainWriter struct {
func (p *plainWriter) Start(ctx context.Context, operation string) { func (p *plainWriter) Start(ctx context.Context, operation string) {
} }
func (p *plainWriter) Event(e Event) { func (p *plainWriter) Event(e api.Resource) {
prefix := "" prefix := ""
if p.dryRun { if p.dryRun {
prefix = api.DRYRUN_PREFIX prefix = api.DRYRUN_PREFIX
@ -46,7 +46,7 @@ func (p *plainWriter) Event(e Event) {
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.Details) _, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.Details)
} }
func (p *plainWriter) On(events ...Event) { func (p *plainWriter) On(events ...api.Resource) {
for _, e := range events { for _, e := range events {
p.Event(e) p.Event(e)
} }

View File

@ -14,11 +14,15 @@
limitations under the License. limitations under the License.
*/ */
package progress package display
import "context" import (
"context"
func NewQuietWriter() EventProcessor { "github.com/docker/compose/v2/pkg/api"
)
func Quiet() api.EventProcessor {
return &quiet{} return &quiet{}
} }
@ -30,5 +34,5 @@ func (q *quiet) Start(_ context.Context, _ string) {
func (q *quiet) Done(_ string, _ bool) { func (q *quiet) Done(_ string, _ bool) {
} }
func (q *quiet) On(_ ...Event) { func (q *quiet) On(_ ...api.Resource) {
} }

View File

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
package progress package display
import ( import (
"runtime" "runtime"

View File

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
package progress package display
import ( import (
"context" "context"
@ -31,11 +31,12 @@ import (
"github.com/morikuni/aec" "github.com/morikuni/aec"
) )
// NewTTYWriter creates an EventProcessor that render advanced UI within a terminal. // Full creates an EventProcessor that render advanced UI within a terminal.
// On Start, TUI lists task with a progress timer // On Start, TUI lists task with a progress timer
func NewTTYWriter(out io.Writer) EventProcessor { func Full(out io.Writer, info io.Writer) api.EventProcessor {
return &ttyWriter{ return &ttyWriter{
out: out, out: out,
info: info,
tasks: map[string]task{}, tasks: map[string]task{},
done: make(chan bool), done: make(chan bool),
mtx: &sync.Mutex{}, mtx: &sync.Mutex{},
@ -55,6 +56,7 @@ type ttyWriter struct {
operation string operation string
ticker *time.Ticker ticker *time.Ticker
suspended bool suspended bool
info io.Writer
} }
type task struct { type task struct {
@ -64,7 +66,7 @@ type task struct {
endTime time.Time endTime time.Time
text string text string
details string details string
status EventStatus status api.EventStatus
current int64 current int64
percent int percent int
total int64 total int64
@ -108,11 +110,16 @@ func (w *ttyWriter) Done(operation string, success bool) {
w.done <- true w.done <- true
} }
func (w *ttyWriter) On(events ...Event) { func (w *ttyWriter) On(events ...api.Resource) {
w.mtx.Lock() w.mtx.Lock()
defer w.mtx.Unlock() defer w.mtx.Unlock()
for _, e := range events { for _, e := range events {
if w.operation != "start" && (e.Text == StatusStarted || e.Text == StatusStarting) { if e.ID == "Compose" {
_, _ = fmt.Fprintln(w.info, ErrorColor(e.Details))
continue
}
if w.operation != "start" && (e.Text == api.StatusStarted || e.Text == api.StatusStarting) {
// skip those events to avoid mix with container logs // skip those events to avoid mix with container logs
continue continue
} }
@ -120,9 +127,9 @@ func (w *ttyWriter) On(events ...Event) {
} }
} }
func (w *ttyWriter) event(e Event) { func (w *ttyWriter) event(e api.Resource) {
// Suspend print while a build is in progress, to avoid collision with buildkit Display // Suspend print while a build is in progress, to avoid collision with buildkit Display
if e.Text == StatusBuilding { if e.Text == api.StatusBuilding {
w.ticker.Stop() w.ticker.Stop()
w.suspended = true w.suspended = true
} else if w.suspended { } else if w.suspended {
@ -132,11 +139,11 @@ func (w *ttyWriter) event(e Event) {
if last, ok := w.tasks[e.ID]; ok { if last, ok := w.tasks[e.ID]; ok {
switch e.Status { switch e.Status {
case Done, Error, Warning: case api.Done, api.Error, api.Warning:
if last.status != e.Status { if last.status != e.Status {
last.stop() last.stop()
} }
case Working: case api.Working:
last.hasMore() last.hasMore()
} }
last.status = e.Status last.status = e.Status
@ -170,7 +177,7 @@ func (w *ttyWriter) event(e Event) {
total: e.Total, total: e.Total,
spinner: NewSpinner(), spinner: NewSpinner(),
} }
if e.Status == Done || e.Status == Error { if e.Status == api.Done || e.Status == api.Error {
t.stop() t.stop()
} }
w.tasks[e.ID] = t w.tasks[e.ID] = t
@ -179,7 +186,7 @@ func (w *ttyWriter) event(e Event) {
w.printEvent(e) w.printEvent(e)
} }
func (w *ttyWriter) printEvent(e Event) { func (w *ttyWriter) printEvent(e api.Resource) {
if w.operation != "" { if w.operation != "" {
// event will be displayed by progress UI on ticker's ticks // event will be displayed by progress UI on ticker's ticks
return return
@ -187,13 +194,13 @@ func (w *ttyWriter) printEvent(e Event) {
var color colorFunc var color colorFunc
switch e.Status { switch e.Status {
case Working: case api.Working:
color = SuccessColor color = SuccessColor
case Done: case api.Done:
color = SuccessColor color = SuccessColor
case Warning: case api.Warning:
color = WarningColor color = WarningColor
case Error: case api.Error:
color = ErrorColor color = ErrorColor
} }
_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details) _, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details)
@ -271,7 +278,7 @@ func (w *ttyWriter) print() {
func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding int, dryRun bool) string { func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding int, dryRun bool) string {
endTime := time.Now() endTime := time.Now()
if t.status != Working { if t.status != api.Working {
endTime = t.startTime endTime = t.startTime
if (t.endTime != time.Time{}) { if (t.endTime != time.Time{}) {
endTime = t.endTime endTime = t.endTime
@ -292,11 +299,11 @@ func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding in
) )
// only show the aggregated progress while the root operation is in-progress // only show the aggregated progress while the root operation is in-progress
if parent := t; parent.status == Working { if parent := t; parent.status == api.Working {
for _, id := range w.ids { for _, id := range w.ids {
child := w.tasks[id] child := w.tasks[id]
if child.parentID == parent.ID { if child.parentID == parent.ID {
if child.status == Working && child.total == 0 { if child.status == api.Working && child.total == 0 {
// we don't have totals available for all the child events // we don't have totals available for all the child events
// so don't show the total progress yet // so don't show the total progress yet
hideDetails = true hideDetails = true
@ -361,24 +368,24 @@ var (
func spinner(t task) string { func spinner(t task) string {
switch t.status { switch t.status {
case Done: case api.Done:
return SuccessColor(spinnerDone) return SuccessColor(spinnerDone)
case Warning: case api.Warning:
return WarningColor(spinnerWarning) return WarningColor(spinnerWarning)
case Error: case api.Error:
return ErrorColor(spinnerError) return ErrorColor(spinnerError)
default: default:
return CountColor(t.spinner.String()) return CountColor(t.spinner.String())
} }
} }
func colorFn(s EventStatus) colorFunc { func colorFn(s api.EventStatus) colorFunc {
switch s { switch s {
case Done: case api.Done:
return SuccessColor return SuccessColor
case Warning: case api.Warning:
return WarningColor return WarningColor
case Error: case api.Error:
return ErrorColor return ErrorColor
default: default:
return nocolor return nocolor
@ -388,7 +395,7 @@ func colorFn(s EventStatus) colorFunc {
func numDone(tasks map[string]task) int { func numDone(tasks map[string]task) int {
i := 0 i := 0
for _, t := range tasks { for _, t := range tasks {
if t.status != Working { if t.status != api.Working {
i++ i++
} }
} }

View File

@ -35,12 +35,7 @@ var (
ErrForbidden = errors.New("forbidden") ErrForbidden = errors.New("forbidden")
// ErrUnknown is returned when the error type is unmapped // ErrUnknown is returned when the error type is unmapped
ErrUnknown = errors.New("unknown") ErrUnknown = errors.New("unknown")
// ErrLoginFailed is returned when login failed // ErrNotImplemented is returned when a backend doesn't implement an action
ErrLoginFailed = errors.New("login failed")
// ErrLoginRequired is returned when login is required for a specific action
ErrLoginRequired = errors.New("login required")
// ErrNotImplemented is returned when a backend doesn't implement
// an action
ErrNotImplemented = errors.New("not implemented") ErrNotImplemented = errors.New("not implemented")
// ErrUnsupportedFlag is returned when a backend doesn't support a flag // ErrUnsupportedFlag is returned when a backend doesn't support a flag
ErrUnsupportedFlag = errors.New("unsupported flag") ErrUnsupportedFlag = errors.New("unsupported flag")
@ -48,9 +43,8 @@ var (
ErrCanceled = errors.New("canceled") ErrCanceled = errors.New("canceled")
// ErrParsingFailed is returned when a string cannot be parsed // ErrParsingFailed is returned when a string cannot be parsed
ErrParsingFailed = errors.New("parsing failed") ErrParsingFailed = errors.New("parsing failed")
// ErrWrongContextType is returned when the caller tries to get a context // ErrNoResources is returned when operation didn't selected any resource
// with the wrong type ErrNoResources = errors.New("no resources")
ErrWrongContextType = errors.New("wrong context type")
) )
// IsNotFoundError returns true if the unwrapped error is ErrNotFound // IsNotFoundError returns true if the unwrapped error is ErrNotFound

103
pkg/api/event.go Normal file
View File

@ -0,0 +1,103 @@
/*
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 api
import (
"context"
)
// EventStatus indicates the status of an action
type EventStatus int
const (
// Working means that the current task is working
Working EventStatus = iota
// Done means that the current task is done
Done
// Warning means that the current task has warning
Warning
// Error means that the current task has errored
Error
)
// ResourceCompose is a special resource ID used when event applies to all resources in the application
const ResourceCompose = "Compose"
const (
StatusError = "Error"
StatusCreating = "Creating"
StatusStarting = "Starting"
StatusStarted = "Started"
StatusWaiting = "Waiting"
StatusHealthy = "Healthy"
StatusExited = "Exited"
StatusRestarting = "Restarting"
StatusRestarted = "Restarted"
StatusRunning = "Running"
StatusCreated = "Created"
StatusStopping = "Stopping"
StatusStopped = "Stopped"
StatusKilling = "Killing"
StatusKilled = "Killed"
StatusRemoving = "Removing"
StatusRemoved = "Removed"
StatusBuilding = "Building"
StatusBuilt = "Built"
StatusPulling = "Pulling"
StatusPulled = "Pulled"
StatusCommitting = "Committing"
StatusCommitted = "Committed"
StatusCopying = "Copying"
StatusCopied = "Copied"
StatusExporting = "Exporting"
StatusExported = "Exported"
)
// Resource represents status change and progress for a compose resource.
type Resource struct {
ID string
ParentID string
Text string
Details string
Status EventStatus
Current int64
Percent int
Total int64
}
func (e *Resource) StatusText() string {
switch e.Status {
case Working:
return "Working"
case Warning:
return "Warning"
case Done:
return "Done"
default:
return "Error"
}
}
// EventProcessor is notified about Compose operations and tasks
type EventProcessor interface {
// Start is triggered as a Compose operation is starting with context
Start(ctx context.Context, operation string)
// On notify about (sub)task and progress processing operation
On(events ...Resource)
// Done is triggered as a Compose operation completed
Done(operation string, success bool)
}

View File

@ -26,7 +26,6 @@ import (
"github.com/containerd/platforms" "github.com/containerd/platforms"
"github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
specs "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -37,7 +36,7 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
if err != nil { if err != nil {
return err return err
} }
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project), return tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project),
func(ctx context.Context) error { func(ctx context.Context) error {
_, err := s.build(ctx, project, options, nil) _, err := s.build(ctx, project, options, nil)

View File

@ -40,7 +40,6 @@ import (
"github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/cli/command/image/build"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/moby/buildkit/client" "github.com/moby/buildkit/client"
@ -118,10 +117,10 @@ type buildStatus struct {
func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
eg := errgroup.Group{} eg := errgroup.Group{}
ch := make(chan *client.SolveStatus) ch := make(chan *client.SolveStatus)
if options.Progress == progress.ModeAuto { displayMode := progressui.DisplayMode(options.Progress)
if displayMode == progressui.AutoMode {
options.Progress = os.Getenv("BUILDKIT_PROGRESS") options.Progress = os.Getenv("BUILDKIT_PROGRESS")
} }
displayMode := progressui.DisplayMode(options.Progress)
out := options.Out out := options.Out
if out == nil { if out == nil {
out = s.stdout() out = s.stdout()
@ -206,7 +205,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
} }
image := api.GetImageNameOrDefault(service, project.Name) image := api.GetImageNameOrDefault(service, project.Name)
s.events.On(progress.BuildingEvent(image)) s.events.On(buildingEvent(image))
expectedImages[serviceName] = image expectedImages[serviceName] = image
@ -408,7 +407,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
return nil, fmt.Errorf("build result not found in Bake metadata for service %s", name) return nil, fmt.Errorf("build result not found in Bake metadata for service %s", name)
} }
results[image] = built.Digest results[image] = built.Digest
s.events.On(progress.BuiltEvent(image)) s.events.On(builtEvent(image))
} }
return results, nil return results, nil
} }
@ -554,20 +553,20 @@ func (s composeService) dryRunBake(cfg bakeConfig) map[string]string {
bakeResponse[name] = dryRunUUID bakeResponse[name] = dryRunUUID
} }
for name := range bakeResponse { for name := range bakeResponse {
s.events.On(progress.BuiltEvent(name)) s.events.On(builtEvent(name))
} }
return bakeResponse return bakeResponse
} }
func (s composeService) displayDryRunBuildEvent(name, dryRunUUID, tag string) { func (s composeService) displayDryRunBuildEvent(name, dryRunUUID, tag string) {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name + " ==>", ID: name + " ==>",
Status: progress.Done, Status: api.Done,
Text: fmt.Sprintf("==> writing image %s", dryRunUUID), Text: fmt.Sprintf("==> writing image %s", dryRunUUID),
}) })
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name + " ==> ==>", ID: name + " ==> ==>",
Status: progress.Done, Status: api.Done,
Text: fmt.Sprintf(`naming to %s`, tag), Text: fmt.Sprintf(`naming to %s`, tag),
}) })
} }

View File

@ -30,7 +30,6 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/cli/command/image/build"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
progress2 "github.com/docker/compose/v2/pkg/progress"
buildtypes "github.com/docker/docker/api/types/build" buildtypes "github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/registry"
@ -86,12 +85,12 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj
} }
image := api.GetImageNameOrDefault(service, project.Name) image := api.GetImageNameOrDefault(service, project.Name)
s.events.On(progress2.BuildingEvent(image)) s.events.On(buildingEvent(image))
id, err := s.doBuildImage(ctx, project, service, options) id, err := s.doBuildImage(ctx, project, service, options)
if err != nil { if err != nil {
return err return err
} }
s.events.On(progress2.BuiltEvent(image)) s.events.On(builtEvent(image))
builtDigests[getServiceIndex(name)] = id builtDigests[getServiceIndex(name)] = id
if options.Push { if options.Push {
@ -258,7 +257,7 @@ func (s *composeService) doBuildImage(ctx context.Context, project *types.Projec
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
s.events.On(progress2.BuildingEvent(imageName)) s.events.On(buildingEvent(imageName))
response, err := s.apiClient().ImageBuild(ctx, body, buildOpts) response, err := s.apiClient().ImageBuild(ctx, body, buildOpts)
if err != nil { if err != nil {
return "", err return "", err
@ -287,7 +286,7 @@ func (s *composeService) doBuildImage(ctx context.Context, project *types.Projec
} }
return "", err return "", err
} }
s.events.On(progress2.BuiltEvent(imageName)) s.events.On(builtEvent(imageName))
return imageID, nil return imageID, nil
} }

View File

@ -22,12 +22,11 @@ import (
"strings" "strings"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
) )
func (s *composeService) Commit(ctx context.Context, projectName string, options api.CommitOptions) error { func (s *composeService) Commit(ctx context.Context, projectName string, options api.CommitOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.commit(ctx, projectName, options) return s.commit(ctx, projectName, options)
}, "commit", s.events) }, "commit", s.events)
} }
@ -42,17 +41,17 @@ func (s *composeService) commit(ctx context.Context, projectName string, options
name := getCanonicalContainerName(ctr) name := getCanonicalContainerName(ctr)
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name, ID: name,
Status: progress.Working, Status: api.Working,
Text: progress.StatusCommitting, Text: api.StatusCommitting,
}) })
if s.dryRun { if s.dryRun {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name, ID: name,
Status: progress.Done, Status: api.Done,
Text: progress.StatusCommitted, Text: api.StatusCommitted,
}) })
return nil return nil
@ -69,10 +68,10 @@ func (s *composeService) commit(ctx context.Context, projectName string, options
return err return err
} }
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name, ID: name,
Text: fmt.Sprintf("Committed as %s", response.ID), Text: fmt.Sprintf("Committed as %s", response.ID),
Status: progress.Done, Status: api.Done,
}) })
return nil return nil

View File

@ -21,7 +21,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -32,7 +31,6 @@ import (
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
@ -45,15 +43,6 @@ import (
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
) )
var stdioToStdout bool
func init() {
out, ok := os.LookupEnv("COMPOSE_STATUS_STDOUT")
if ok {
stdioToStdout, _ = strconv.ParseBool(out)
}
}
type Option func(service *composeService) error type Option func(service *composeService) error
// NewComposeService creates a Compose service using Docker CLI. // NewComposeService creates a Compose service using Docker CLI.
@ -96,7 +85,7 @@ func NewComposeService(dockerCli command.Cli, options ...Option) (api.Compose, e
} }
} }
if s.events == nil { if s.events == nil {
s.events = progress.NewQuietWriter() s.events = &ignore{}
} }
// If custom streams were provided, wrap the Docker CLI to use them // If custom streams were provided, wrap the Docker CLI to use them
@ -204,7 +193,7 @@ func AlwaysOkPrompt() Prompt {
// WithEventProcessor configure component to get notified on Compose operation and progress events. // WithEventProcessor configure component to get notified on Compose operation and progress events.
// Typically used to configure a progress UI // Typically used to configure a progress UI
func WithEventProcessor(bus progress.EventProcessor) Option { func WithEventProcessor(bus api.EventProcessor) Option {
return func(s *composeService) error { return func(s *composeService) error {
s.events = bus s.events = bus
return nil return nil
@ -216,7 +205,7 @@ type composeService struct {
// prompt is used to interact with user and confirm actions // prompt is used to interact with user and confirm actions
prompt Prompt prompt Prompt
// eventBus collects tasks execution events // eventBus collects tasks execution events
events progress.EventProcessor events api.EventProcessor
// Optional overrides for specific components (for SDK users) // Optional overrides for specific components (for SDK users)
outStream io.Writer outStream io.Writer
@ -278,13 +267,6 @@ func (s *composeService) stderr() *streams.Out {
return s.dockerCli.Err() return s.dockerCli.Err()
} }
func (s *composeService) stdinfo() *streams.Out {
if stdioToStdout {
return s.stdout()
}
return s.stderr()
}
// readCloserAdapter adapts io.Reader to io.ReadCloser // readCloserAdapter adapts io.Reader to io.ReadCloser
type readCloserAdapter struct { type readCloserAdapter struct {
r io.Reader r io.Reader

View File

@ -41,7 +41,6 @@ import (
"github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
) )
@ -187,7 +186,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
name := getContainerProgressName(ctr) name := getContainerProgressName(ctr)
switch ctr.State { switch ctr.State {
case container.StateRunning: case container.StateRunning:
c.compose.events.On(progress.RunningEvent(name)) c.compose.events.On(runningEvent(name))
case container.StateCreated: case container.StateCreated:
case container.StateRestarting: case container.StateRestarting:
case container.StateExited: case container.StateExited:
@ -426,16 +425,16 @@ func getContainerProgressName(ctr container.Summary) string {
return "Container " + getCanonicalContainerName(ctr) return "Container " + getCanonicalContainerName(ctr)
} }
func containerEvents(containers Containers, eventFunc func(string) progress.Event) []progress.Event { func containerEvents(containers Containers, eventFunc func(string) api.Resource) []api.Resource {
events := []progress.Event{} events := []api.Resource{}
for _, ctr := range containers { for _, ctr := range containers {
events = append(events, eventFunc(getContainerProgressName(ctr))) events = append(events, eventFunc(getContainerProgressName(ctr)))
} }
return events return events
} }
func containerReasonEvents(containers Containers, eventFunc func(string, string) progress.Event, reason string) []progress.Event { func containerReasonEvents(containers Containers, eventFunc func(string, string) api.Resource, reason string) []api.Resource {
events := []progress.Event{} events := []api.Resource{}
for _, ctr := range containers { for _, ctr := range containers {
events = append(events, eventFunc(getContainerProgressName(ctr), reason)) events = append(events, eventFunc(getContainerProgressName(ctr), reason))
} }
@ -461,7 +460,7 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
} }
waitingFor := containers.filter(isService(dep), isNotOneOff) waitingFor := containers.filter(isService(dep), isNotOneOff)
s.events.On(containerEvents(waitingFor, progress.Waiting)...) s.events.On(containerEvents(waitingFor, waiting)...)
if len(waitingFor) == 0 { if len(waitingFor) == 0 {
if config.Required { if config.Required {
return fmt.Errorf("%s is missing dependency %s", dependant, dep) return fmt.Errorf("%s is missing dependency %s", dependant, dep)
@ -481,61 +480,61 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
} }
switch config.Condition { switch config.Condition {
case ServiceConditionRunningOrHealthy: case ServiceConditionRunningOrHealthy:
healthy, err := s.isServiceHealthy(ctx, waitingFor, true) isHealthy, err := s.isServiceHealthy(ctx, waitingFor, true)
if err != nil { if err != nil {
if !config.Required { if !config.Required {
s.events.On(containerReasonEvents(waitingFor, progress.SkippedEvent, s.events.On(containerReasonEvents(waitingFor, skippedEvent,
fmt.Sprintf("optional dependency %q is not running or is unhealthy", dep))...) fmt.Sprintf("optional dependency %q is not running or is unhealthy", dep))...)
logrus.Warnf("optional dependency %q is not running or is unhealthy: %s", dep, err.Error()) logrus.Warnf("optional dependency %q is not running or is unhealthy: %s", dep, err.Error())
return nil return nil
} }
return err return err
} }
if healthy { if isHealthy {
s.events.On(containerEvents(waitingFor, progress.Healthy)...) s.events.On(containerEvents(waitingFor, healthy)...)
return nil return nil
} }
case types.ServiceConditionHealthy: case types.ServiceConditionHealthy:
healthy, err := s.isServiceHealthy(ctx, waitingFor, false) isHealthy, err := s.isServiceHealthy(ctx, waitingFor, false)
if err != nil { if err != nil {
if !config.Required { if !config.Required {
s.events.On(containerReasonEvents(waitingFor, progress.SkippedEvent, s.events.On(containerReasonEvents(waitingFor, skippedEvent,
fmt.Sprintf("optional dependency %q failed to start", dep))...) fmt.Sprintf("optional dependency %q failed to start", dep))...)
logrus.Warnf("optional dependency %q failed to start: %s", dep, err.Error()) logrus.Warnf("optional dependency %q failed to start: %s", dep, err.Error())
return nil return nil
} }
s.events.On(containerEvents(waitingFor, func(s string) progress.Event { s.events.On(containerEvents(waitingFor, func(s string) api.Resource {
return progress.ErrorEventf(s, "dependency %s failed to start", dep) return errorEventf(s, "dependency %s failed to start", dep)
})...) })...)
return fmt.Errorf("dependency failed to start: %w", err) return fmt.Errorf("dependency failed to start: %w", err)
} }
if healthy { if isHealthy {
s.events.On(containerEvents(waitingFor, progress.Healthy)...) s.events.On(containerEvents(waitingFor, healthy)...)
return nil return nil
} }
case types.ServiceConditionCompletedSuccessfully: case types.ServiceConditionCompletedSuccessfully:
exited, code, err := s.isServiceCompleted(ctx, waitingFor) isExited, code, err := s.isServiceCompleted(ctx, waitingFor)
if err != nil { if err != nil {
return err return err
} }
if exited { if isExited {
if code == 0 { if code == 0 {
s.events.On(containerEvents(waitingFor, progress.Exited)...) s.events.On(containerEvents(waitingFor, exited)...)
return nil return nil
} }
messageSuffix := fmt.Sprintf("%q didn't complete successfully: exit %d", dep, code) messageSuffix := fmt.Sprintf("%q didn't complete successfully: exit %d", dep, code)
if !config.Required { if !config.Required {
// optional -> mark as skipped & don't propagate error // optional -> mark as skipped & don't propagate error
s.events.On(containerReasonEvents(waitingFor, progress.SkippedEvent, s.events.On(containerReasonEvents(waitingFor, skippedEvent,
fmt.Sprintf("optional dependency %s", messageSuffix))...) fmt.Sprintf("optional dependency %s", messageSuffix))...)
logrus.Warnf("optional dependency %s", messageSuffix) logrus.Warnf("optional dependency %s", messageSuffix)
return nil return nil
} }
msg := fmt.Sprintf("service %s", messageSuffix) msg := fmt.Sprintf("service %s", messageSuffix)
s.events.On(containerEvents(waitingFor, func(s string) progress.Event { s.events.On(containerEvents(waitingFor, func(s string) api.Resource {
return progress.ErrorEventf(s, "service %s", messageSuffix) return errorEventf(s, "service %s", messageSuffix)
})...) })...)
return errors.New(msg) return errors.New(msg)
} }
@ -599,19 +598,19 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro
name string, number int, opts createOptions, name string, number int, opts createOptions,
) (ctr container.Summary, err error) { ) (ctr container.Summary, err error) {
eventName := "Container " + name eventName := "Container " + name
s.events.On(progress.CreatingEvent(eventName)) s.events.On(creatingEvent(eventName))
ctr, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts) ctr, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts)
if err != nil { if err != nil {
if ctx.Err() == nil { if ctx.Err() == nil {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: eventName, ID: eventName,
Status: progress.Error, Status: api.Error,
Text: err.Error(), Text: err.Error(),
}) })
} }
return ctr, err return ctr, err
} }
s.events.On(progress.CreatedEvent(eventName)) s.events.On(createdEvent(eventName))
return ctr, nil return ctr, nil
} }
@ -619,12 +618,12 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
replaced container.Summary, inherit bool, timeout *time.Duration, replaced container.Summary, inherit bool, timeout *time.Duration,
) (created container.Summary, err error) { ) (created container.Summary, err error) {
eventName := getContainerProgressName(replaced) eventName := getContainerProgressName(replaced)
s.events.On(progress.NewEvent(eventName, progress.Working, "Recreate")) s.events.On(newEvent(eventName, api.Working, "Recreate"))
defer func() { defer func() {
if err != nil && ctx.Err() == nil { if err != nil && ctx.Err() == nil {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: eventName, ID: eventName,
Status: progress.Error, Status: api.Error,
Text: err.Error(), Text: err.Error(),
}) })
} }
@ -673,7 +672,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
return created, err return created, err
} }
s.events.On(progress.NewEvent(eventName, progress.Done, "Recreated")) s.events.On(newEvent(eventName, api.Done, "Recreated"))
return created, err return created, err
} }
@ -681,14 +680,14 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
var startMx sync.Mutex var startMx sync.Mutex
func (s *composeService) startContainer(ctx context.Context, ctr container.Summary) error { func (s *composeService) startContainer(ctx context.Context, ctr container.Summary) error {
s.events.On(progress.NewEvent(getContainerProgressName(ctr), progress.Working, "Restart")) s.events.On(newEvent(getContainerProgressName(ctr), api.Working, "Restart"))
startMx.Lock() startMx.Lock()
defer startMx.Unlock() defer startMx.Unlock()
err := s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{}) err := s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{})
if err != nil { if err != nil {
return err return err
} }
s.events.On(progress.NewEvent(getContainerProgressName(ctr), progress.Done, "Restarted")) s.events.On(newEvent(getContainerProgressName(ctr), api.Done, "Restarted"))
return nil return nil
} }
@ -719,9 +718,9 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
return created, err return created, err
} }
for _, warning := range response.Warnings { for _, warning := range response.Warnings {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: service.Name, ID: service.Name,
Status: progress.Warning, Status: api.Warning,
Text: warning, Text: warning,
}) })
} }
@ -906,7 +905,7 @@ func (s *composeService) startService(ctx context.Context,
} }
eventName := getContainerProgressName(ctr) eventName := getContainerProgressName(ctr)
s.events.On(progress.StartingEvent(eventName)) s.events.On(startingEvent(eventName))
err = s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{}) err = s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{})
if err != nil { if err != nil {
return err return err
@ -919,7 +918,7 @@ func (s *composeService) startService(ctx context.Context,
} }
} }
s.events.On(progress.StartedEvent(eventName)) s.events.On(startedEvent(eventName))
} }
return nil return nil
} }

View File

@ -25,7 +25,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/docker/compose/v2/pkg/progress"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
@ -43,7 +42,7 @@ const (
) )
func (s *composeService) Copy(ctx context.Context, projectName string, options api.CopyOptions) error { func (s *composeService) Copy(ctx context.Context, projectName string, options api.CopyOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.copy(ctx, projectName, options) return s.copy(ctx, projectName, options)
}, "copy", s.events) }, "copy", s.events)
} }
@ -90,20 +89,20 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
} else { } else {
msg = fmt.Sprintf("%s to %s:%s", srcPath, name, dstPath) msg = fmt.Sprintf("%s to %s:%s", srcPath, name, dstPath)
} }
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name, ID: name,
Text: progress.StatusCopying, Text: api.StatusCopying,
Details: msg, Details: msg,
Status: progress.Working, Status: api.Working,
}) })
if err := copyFunc(ctx, ctr.ID, srcPath, dstPath, options); err != nil { if err := copyFunc(ctx, ctr.ID, srcPath, dstPath, options); err != nil {
return err return err
} }
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name, ID: name,
Text: progress.StatusCopied, Text: api.StatusCopied,
Details: msg, Details: msg,
Status: progress.Done, Status: api.Done,
}) })
return nil return nil
}) })

View File

@ -43,7 +43,6 @@ import (
cdi "tags.cncf.io/container-device-interface/pkg/parser" cdi "tags.cncf.io/container-device-interface/pkg/parser"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
) )
type createOptions struct { type createOptions struct {
@ -61,7 +60,7 @@ type createConfigs struct {
} }
func (s *composeService) Create(ctx context.Context, project *types.Project, createOpts api.CreateOptions) error { func (s *composeService) Create(ctx context.Context, project *types.Project, createOpts api.CreateOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.create(ctx, project, createOpts) return s.create(ctx, project, createOpts)
}, "create", s.events) }, "create", s.events)
} }
@ -1394,14 +1393,14 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
} }
networkEventName := fmt.Sprintf("Network %s", n.Name) networkEventName := fmt.Sprintf("Network %s", n.Name)
s.events.On(progress.CreatingEvent(networkEventName)) s.events.On(creatingEvent(networkEventName))
resp, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts) resp, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts)
if err != nil { if err != nil {
s.events.On(progress.ErrorEvent(networkEventName, err.Error())) s.events.On(errorEvent(networkEventName, err.Error()))
return "", fmt.Errorf("failed to create network %s: %w", n.Name, err) return "", fmt.Errorf("failed to create network %s: %w", n.Name, err)
} }
s.events.On(progress.CreatedEvent(networkEventName)) s.events.On(createdEvent(networkEventName))
err = s.connectNetwork(ctx, n.Name, dangledContainers, nil) err = s.connectNetwork(ctx, n.Name, dangledContainers, nil)
if err != nil { if err != nil {
@ -1443,7 +1442,7 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
err = s.apiClient().NetworkRemove(ctx, n.Name) err = s.apiClient().NetworkRemove(ctx, n.Name)
eventName := fmt.Sprintf("Network %s", n.Name) eventName := fmt.Sprintf("Network %s", n.Name)
s.events.On(progress.RemovedEvent(eventName)) s.events.On(removedEvent(eventName))
return containers, err return containers, err
} }
@ -1619,7 +1618,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error { func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {
eventName := fmt.Sprintf("Volume %s", volume.Name) eventName := fmt.Sprintf("Volume %s", volume.Name)
s.events.On(progress.CreatingEvent(eventName)) s.events.On(creatingEvent(eventName))
hash, err := VolumeHash(volume) hash, err := VolumeHash(volume)
if err != nil { if err != nil {
return err return err
@ -1632,9 +1631,9 @@ func (s *composeService) createVolume(ctx context.Context, volume types.VolumeCo
DriverOpts: volume.DriverOpts, DriverOpts: volume.DriverOpts,
}) })
if err != nil { if err != nil {
s.events.On(progress.ErrorEvent(eventName, err.Error())) s.events.On(errorEvent(eventName, err.Error()))
return err return err
} }
s.events.On(progress.CreatedEvent(eventName)) s.events.On(createdEvent(eventName))
return nil return nil
} }

View File

@ -25,7 +25,6 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
containerType "github.com/docker/docker/api/types/container" containerType "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -38,7 +37,7 @@ import (
type downOp func() error type downOp func() error
func (s *composeService) Down(ctx context.Context, projectName string, options api.DownOptions) error { func (s *composeService) Down(ctx context.Context, projectName string, options api.DownOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.down(ctx, strings.ToLower(projectName), options) return s.down(ctx, strings.ToLower(projectName), options)
}, "down", s.events) }, "down", s.events)
} }
@ -210,7 +209,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
} }
eventName := fmt.Sprintf("Network %s", name) eventName := fmt.Sprintf("Network %s", name)
s.events.On(progress.RemovingEvent(eventName)) s.events.On(removingEvent(eventName))
var found int var found int
for _, net := range networks { for _, net := range networks {
@ -219,14 +218,14 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
} }
nw, err := s.apiClient().NetworkInspect(ctx, net.ID, network.InspectOptions{}) nw, err := s.apiClient().NetworkInspect(ctx, net.ID, network.InspectOptions{})
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
s.events.On(progress.NewEvent(eventName, progress.Warning, "No resource found to remove")) s.events.On(newEvent(eventName, api.Warning, "No resource found to remove"))
return nil return nil
} }
if err != nil { if err != nil {
return err return err
} }
if len(nw.Containers) > 0 { if len(nw.Containers) > 0 {
s.events.On(progress.NewEvent(eventName, progress.Warning, "Resource is still in use")) s.events.On(newEvent(eventName, api.Warning, "Resource is still in use"))
found++ found++
continue continue
} }
@ -235,10 +234,10 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
continue continue
} }
s.events.On(progress.ErrorEvent(eventName, err.Error())) s.events.On(errorEvent(eventName, err.Error()))
return fmt.Errorf("failed to remove network %s: %w", name, err) return fmt.Errorf("failed to remove network %s: %w", name, err)
} }
s.events.On(progress.RemovedEvent(eventName)) s.events.On(removedEvent(eventName))
found++ found++
} }
@ -246,7 +245,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
// in practice, it's extremely unlikely for this to ever occur, as it'd // in practice, it's extremely unlikely for this to ever occur, as it'd
// mean the network was present when we queried at the start of this // mean the network was present when we queried at the start of this
// method but was then deleted by something else in the interim // method but was then deleted by something else in the interim
s.events.On(progress.NewEvent(eventName, progress.Warning, "No resource found to remove")) s.events.On(newEvent(eventName, api.Warning, "No resource found to remove"))
return nil return nil
} }
return nil return nil
@ -254,18 +253,18 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
func (s *composeService) removeImage(ctx context.Context, image string) error { func (s *composeService) removeImage(ctx context.Context, image string) error {
id := fmt.Sprintf("Image %s", image) id := fmt.Sprintf("Image %s", image)
s.events.On(progress.NewEvent(id, progress.Working, "Removing")) s.events.On(newEvent(id, api.Working, "Removing"))
_, err := s.apiClient().ImageRemove(ctx, image, imageapi.RemoveOptions{}) _, err := s.apiClient().ImageRemove(ctx, image, imageapi.RemoveOptions{})
if err == nil { if err == nil {
s.events.On(progress.NewEvent(id, progress.Done, "Removed")) s.events.On(newEvent(id, api.Done, "Removed"))
return nil return nil
} }
if errdefs.IsConflict(err) { if errdefs.IsConflict(err) {
s.events.On(progress.NewEvent(id, progress.Warning, "Resource is still in use")) s.events.On(newEvent(id, api.Warning, "Resource is still in use"))
return nil return nil
} }
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
s.events.On(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove")) s.events.On(newEvent(id, api.Done, "Warning: No resource found to remove"))
return nil return nil
} }
return err return err
@ -280,18 +279,18 @@ func (s *composeService) removeVolume(ctx context.Context, id string) error {
return nil return nil
} }
s.events.On(progress.NewEvent(resource, progress.Working, "Removing")) s.events.On(newEvent(resource, api.Working, "Removing"))
err = s.apiClient().VolumeRemove(ctx, id, true) err = s.apiClient().VolumeRemove(ctx, id, true)
if err == nil { if err == nil {
s.events.On(progress.NewEvent(resource, progress.Done, "Removed")) s.events.On(newEvent(resource, api.Done, "Removed"))
return nil return nil
} }
if errdefs.IsConflict(err) { if errdefs.IsConflict(err) {
s.events.On(progress.NewEvent(resource, progress.Warning, "Resource is still in use")) s.events.On(newEvent(resource, api.Warning, "Resource is still in use"))
return nil return nil
} }
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
s.events.On(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove")) s.events.On(newEvent(resource, api.Done, "Warning: No resource found to remove"))
return nil return nil
} }
return err return err
@ -299,7 +298,7 @@ func (s *composeService) removeVolume(ctx context.Context, id string) error {
func (s *composeService) stopContainer(ctx context.Context, service *types.ServiceConfig, ctr containerType.Summary, timeout *time.Duration, listener api.ContainerEventListener) error { func (s *composeService) stopContainer(ctx context.Context, service *types.ServiceConfig, ctr containerType.Summary, timeout *time.Duration, listener api.ContainerEventListener) error {
eventName := getContainerProgressName(ctr) eventName := getContainerProgressName(ctr)
s.events.On(progress.StoppingEvent(eventName)) s.events.On(stoppingEvent(eventName))
if service != nil { if service != nil {
for _, hook := range service.PreStop { for _, hook := range service.PreStop {
@ -317,10 +316,10 @@ func (s *composeService) stopContainer(ctx context.Context, service *types.Servi
timeoutInSecond := utils.DurationSecondToInt(timeout) timeoutInSecond := utils.DurationSecondToInt(timeout)
err := s.apiClient().ContainerStop(ctx, ctr.ID, containerType.StopOptions{Timeout: timeoutInSecond}) err := s.apiClient().ContainerStop(ctx, ctr.ID, containerType.StopOptions{Timeout: timeoutInSecond})
if err != nil { if err != nil {
s.events.On(progress.ErrorEvent(eventName, "Error while Stopping")) s.events.On(errorEvent(eventName, "Error while Stopping"))
return err return err
} }
s.events.On(progress.StoppedEvent(eventName)) s.events.On(stoppedEvent(eventName))
return nil return nil
} }
@ -348,22 +347,22 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
eventName := getContainerProgressName(ctr) eventName := getContainerProgressName(ctr)
err := s.stopContainer(ctx, service, ctr, timeout, nil) err := s.stopContainer(ctx, service, ctr, timeout, nil)
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
s.events.On(progress.RemovedEvent(eventName)) s.events.On(removedEvent(eventName))
return nil return nil
} }
if err != nil { if err != nil {
return err return err
} }
s.events.On(progress.RemovingEvent(eventName)) s.events.On(removingEvent(eventName))
err = s.apiClient().ContainerRemove(ctx, ctr.ID, containerType.RemoveOptions{ err = s.apiClient().ContainerRemove(ctx, ctr.ID, containerType.RemoveOptions{
Force: true, Force: true,
RemoveVolumes: volumes, RemoveVolumes: volumes,
}) })
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) { if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
s.events.On(progress.ErrorEvent(eventName, "Error while Removing")) s.events.On(errorEvent(eventName, "Error while Removing"))
return err return err
} }
s.events.On(progress.RemovedEvent(eventName)) s.events.On(removedEvent(eventName))
return nil return nil
} }

View File

@ -24,12 +24,11 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/moby/sys/atomicwriter" "github.com/moby/sys/atomicwriter"
) )
func (s *composeService) Export(ctx context.Context, projectName string, options api.ExportOptions) error { func (s *composeService) Export(ctx context.Context, projectName string, options api.ExportOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.export(ctx, projectName, options) return s.export(ctx, projectName, options)
}, "export", s.events) }, "export", s.events)
} }
@ -51,10 +50,10 @@ func (s *composeService) export(ctx context.Context, projectName string, options
} }
name := getCanonicalContainerName(container) name := getCanonicalContainerName(container)
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name, ID: name,
Text: progress.StatusExporting, Text: api.StatusExporting,
Status: progress.Working, Status: api.Working,
}) })
responseBody, err := s.apiClient().ContainerExport(ctx, container.ID) responseBody, err := s.apiClient().ContainerExport(ctx, container.ID)
@ -64,7 +63,7 @@ func (s *composeService) export(ctx context.Context, projectName string, options
defer func() { defer func() {
if err := responseBody.Close(); err != nil { if err := responseBody.Close(); err != nil {
s.events.On(progress.ErrorEventf(name, "Failed to close response body: %s", err.Error())) s.events.On(errorEventf(name, "Failed to close response body: %s", err.Error()))
} }
}() }()
@ -84,10 +83,10 @@ func (s *composeService) export(ctx context.Context, projectName string, options
} }
} }
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name, ID: name,
Text: progress.StatusExported, Text: api.StatusExported,
Status: progress.Done, Status: api.Done,
}) })
return nil return nil

View File

@ -18,18 +18,16 @@ package compose
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
) )
func (s *composeService) Kill(ctx context.Context, projectName string, options api.KillOptions) error { func (s *composeService) Kill(ctx context.Context, projectName string, options api.KillOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.kill(ctx, strings.ToLower(projectName), options) return s.kill(ctx, strings.ToLower(projectName), options)
}, "kill", s.events) }, "kill", s.events)
} }
@ -55,21 +53,20 @@ func (s *composeService) kill(ctx context.Context, projectName string, options a
containers = containers.filter(isService(project.ServiceNames()...)) containers = containers.filter(isService(project.ServiceNames()...))
} }
if len(containers) == 0 { if len(containers) == 0 {
_, _ = fmt.Fprintf(s.stdinfo(), "no container to kill") return api.ErrNoResources
return nil
} }
eg, ctx := errgroup.WithContext(ctx) eg, ctx := errgroup.WithContext(ctx)
containers.forEach(func(ctr container.Summary) { containers.forEach(func(ctr container.Summary) {
eg.Go(func() error { eg.Go(func() error {
eventName := getContainerProgressName(ctr) eventName := getContainerProgressName(ctr)
s.events.On(progress.KillingEvent(eventName)) s.events.On(killingEvent(eventName))
err := s.apiClient().ContainerKill(ctx, ctr.ID, options.Signal) err := s.apiClient().ContainerKill(ctx, ctr.ID, options.Signal)
if err != nil { if err != nil {
s.events.On(progress.ErrorEvent(eventName, "Error while Killing")) s.events.On(errorEvent(eventName, "Error while Killing"))
return err return err
} }
s.events.On(progress.KilledEvent(eventName)) s.events.On(killedEvent(eventName))
return nil return nil
}) })
}) })

View File

@ -29,7 +29,7 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/api"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -101,10 +101,10 @@ func (m *modelAPI) Close() {
m.cleanup() m.cleanup()
} }
func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, events progress.EventProcessor) error { func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, events api.EventProcessor) error {
events.On(progress.Event{ events.On(api.Resource{
ID: model.Name, ID: model.Name,
Status: progress.Working, Status: api.Working,
Text: "Pulling", Text: "Pulling",
}) })
@ -131,30 +131,30 @@ func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quiet
} }
if !quietPull { if !quietPull {
events.On(progress.Event{ events.On(api.Resource{
ID: model.Name, ID: model.Name,
Status: progress.Working, Status: api.Working,
Text: progress.StatusPulling, Text: api.StatusPulling,
}) })
} }
} }
err = cmd.Wait() err = cmd.Wait()
if err != nil { if err != nil {
events.On(progress.ErrorEvent(model.Name, err.Error())) events.On(errorEvent(model.Name, err.Error()))
} }
events.On(progress.Event{ events.On(api.Resource{
ID: model.Name, ID: model.Name,
Status: progress.Working, Status: api.Working,
Text: progress.StatusPulled, Text: api.StatusPulled,
}) })
return err return err
} }
func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events progress.EventProcessor) error { func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events api.EventProcessor) error {
events.On(progress.Event{ events.On(api.Resource{
ID: config.Name, ID: config.Name,
Status: progress.Working, Status: api.Working,
Text: "Configuring", Text: "Configuring",
}) })
// configure [--context-size=<n>] MODEL [-- <runtime-flags...>] // configure [--context-size=<n>] MODEL [-- <runtime-flags...>]

View File

@ -24,11 +24,10 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
) )
func (s *composeService) Pause(ctx context.Context, projectName string, options api.PauseOptions) error { func (s *composeService) Pause(ctx context.Context, projectName string, options api.PauseOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.pause(ctx, strings.ToLower(projectName), options) return s.pause(ctx, strings.ToLower(projectName), options)
}, "pause", s.events) }, "pause", s.events)
} }
@ -49,7 +48,7 @@ func (s *composeService) pause(ctx context.Context, projectName string, options
err := s.apiClient().ContainerPause(ctx, container.ID) err := s.apiClient().ContainerPause(ctx, container.ID)
if err == nil { if err == nil {
eventName := getContainerProgressName(container) eventName := getContainerProgressName(container)
s.events.On(progress.NewEvent(eventName, progress.Done, "Paused")) s.events.On(newEvent(eventName, api.Done, "Paused"))
} }
return err return err
}) })
@ -58,7 +57,7 @@ func (s *composeService) pause(ctx context.Context, projectName string, options
} }
func (s *composeService) UnPause(ctx context.Context, projectName string, options api.PauseOptions) error { func (s *composeService) UnPause(ctx context.Context, projectName string, options api.PauseOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.unPause(ctx, strings.ToLower(projectName), options) return s.unPause(ctx, strings.ToLower(projectName), options)
}, "unpause", s.events) }, "unpause", s.events)
} }
@ -79,7 +78,7 @@ func (s *composeService) unPause(ctx context.Context, projectName string, option
err = s.apiClient().ContainerUnpause(ctx, ctr.ID) err = s.apiClient().ContainerUnpause(ctx, ctr.ID)
if err == nil { if err == nil {
eventName := getContainerProgressName(ctr) eventName := getContainerProgressName(ctr)
s.events.On(progress.NewEvent(eventName, progress.Done, "Unpaused")) s.events.On(newEvent(eventName, api.Done, "Unpaused"))
} }
return err return err
}) })

View File

@ -33,7 +33,7 @@ import (
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
"github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/api"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -89,10 +89,10 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
var action string var action string
switch command { switch command {
case "up": case "up":
s.events.On(progress.CreatingEvent(service.Name)) s.events.On(creatingEvent(service.Name))
action = "create" action = "create"
case "down": case "down":
s.events.On(progress.RemovingEvent(service.Name)) s.events.On(removingEvent(service.Name))
action = "remove" action = "remove"
default: default:
return nil, fmt.Errorf("unsupported plugin command: %s", command) return nil, fmt.Errorf("unsupported plugin command: %s", command)
@ -124,10 +124,10 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
} }
switch msg.Type { switch msg.Type {
case ErrorType: case ErrorType:
s.events.On(progress.NewEvent(service.Name, progress.Error, msg.Message)) s.events.On(newEvent(service.Name, api.Error, msg.Message))
return nil, errors.New(msg.Message) return nil, errors.New(msg.Message)
case InfoType: case InfoType:
s.events.On(progress.NewEvent(service.Name, progress.Working, msg.Message)) s.events.On(newEvent(service.Name, api.Working, msg.Message))
case SetEnvType: case SetEnvType:
key, val, found := strings.Cut(msg.Message, "=") key, val, found := strings.Cut(msg.Message, "=")
if !found { if !found {
@ -143,14 +143,14 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
err = cmd.Wait() err = cmd.Wait()
if err != nil { if err != nil {
s.events.On(progress.ErrorEvent(service.Name, err.Error())) s.events.On(errorEvent(service.Name, err.Error()))
return nil, fmt.Errorf("failed to %s service provider: %s", action, err.Error()) return nil, fmt.Errorf("failed to %s service provider: %s", action, err.Error())
} }
switch command { switch command {
case "up": case "up":
s.events.On(progress.CreatedEvent(service.Name)) s.events.On(createdEvent(service.Name))
case "down": case "down":
s.events.On(progress.RemovedEvent(service.Name)) s.events.On(removedEvent(service.Name))
} }
return variables, nil return variables, nil
} }

176
pkg/compose/progress.go Normal file
View File

@ -0,0 +1,176 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"fmt"
"github.com/docker/compose/v2/pkg/api"
)
type progressFunc func(context.Context) error
func Run(ctx context.Context, pf progressFunc, operation string, bus api.EventProcessor) error {
bus.Start(ctx, operation)
err := pf(ctx)
bus.Done(operation, err != nil)
return err
}
// errorEvent creates a new Error Resource with message
func errorEvent(id string, msg string) api.Resource {
return api.Resource{
ID: id,
Status: api.Error,
Text: api.StatusError,
Details: msg,
}
}
// errorEventf creates a new Error Resource with format message
func errorEventf(id string, msg string, args ...any) api.Resource {
return errorEvent(id, fmt.Sprintf(msg, args...))
}
// creatingEvent creates a new Create in progress Resource
func creatingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusCreating)
}
// startingEvent creates a new Starting in progress Resource
func startingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusStarting)
}
// startedEvent creates a new Started in progress Resource
func startedEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusStarted)
}
// waiting creates a new waiting event
func waiting(id string) api.Resource {
return newEvent(id, api.Working, api.StatusWaiting)
}
// healthy creates a new healthy event
func healthy(id string) api.Resource {
return newEvent(id, api.Done, api.StatusHealthy)
}
// exited creates a new exited event
func exited(id string) api.Resource {
return newEvent(id, api.Done, api.StatusExited)
}
// restartingEvent creates a new Restarting in progress Resource
func restartingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusRestarting)
}
// runningEvent creates a new Running in progress Resource
func runningEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusRunning)
}
// createdEvent creates a new Created (done) Resource
func createdEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusCreated)
}
// stoppingEvent creates a new Stopping in progress Resource
func stoppingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusStopping)
}
// stoppedEvent creates a new Stopping in progress Resource
func stoppedEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusStopped)
}
// killingEvent creates a new Killing in progress Resource
func killingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusKilling)
}
// killedEvent creates a new Killed in progress Resource
func killedEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusKilled)
}
// removingEvent creates a new Removing in progress Resource
func removingEvent(id string) api.Resource {
return newEvent(id, api.Working, api.StatusRemoving)
}
// removedEvent creates a new removed (done) Resource
func removedEvent(id string) api.Resource {
return newEvent(id, api.Done, api.StatusRemoved)
}
// buildingEvent creates a new Building in progress Resource
func buildingEvent(id string) api.Resource {
return newEvent("Image "+id, api.Working, api.StatusBuilding)
}
// builtEvent creates a new built (done) Resource
func builtEvent(id string) api.Resource {
return newEvent("Image "+id, api.Done, api.StatusBuilt)
}
// pullingEvent creates a new pulling (in progress) Resource
func pullingEvent(id string) api.Resource {
return newEvent("Image "+id, api.Working, api.StatusPulling)
}
// pulledEvent creates a new pulled (done) Resource
func pulledEvent(id string) api.Resource {
return newEvent("Image "+id, api.Done, api.StatusPulled)
}
// skippedEvent creates a new Skipped Resource
func skippedEvent(id string, reason string) api.Resource {
return api.Resource{
ID: id,
Status: api.Warning,
Text: "Skipped: " + reason,
}
}
// newEvent new event
func newEvent(id string, status api.EventStatus, text string, reason ...string) api.Resource {
r := api.Resource{
ID: id,
Status: status,
Text: text,
}
if len(reason) > 0 {
r.Details = reason[0]
}
return r
}
type ignore struct{}
func (q *ignore) Start(_ context.Context, _ string) {
}
func (q *ignore) Done(_ string, _ bool) {
}
func (q *ignore) On(_ ...api.Resource) {
}

View File

@ -35,7 +35,6 @@ import (
"github.com/docker/compose/v2/internal/oci" "github.com/docker/compose/v2/internal/oci"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose/transform" "github.com/docker/compose/v2/pkg/compose/transform"
"github.com/docker/compose/v2/pkg/progress"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go" "github.com/opencontainers/image-spec/specs-go"
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
@ -43,7 +42,7 @@ import (
) )
func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error { func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.publish(ctx, project, repository, options) return s.publish(ctx, project, repository, options)
}, "publish", s.events) }, "publish", s.events)
} }
@ -71,10 +70,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
return err return err
} }
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: repository, ID: repository,
Text: "publishing", Text: "publishing",
Status: progress.Working, Status: api.Working,
}) })
if logrus.IsLevelEnabled(logrus.DebugLevel) { if logrus.IsLevelEnabled(logrus.DebugLevel) {
logrus.Debug("publishing layers") logrus.Debug("publishing layers")
@ -98,10 +97,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
descriptor, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion) descriptor, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion)
if err != nil { if err != nil {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: repository, ID: repository,
Text: "publishing", Text: "publishing",
Status: progress.Error, Status: api.Error,
}) })
return err return err
} }
@ -150,10 +149,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
} }
} }
} }
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: repository, ID: repository,
Text: "published", Text: "published",
Status: progress.Done, Status: api.Done,
}) })
return nil return nil
} }

View File

@ -40,11 +40,10 @@ import (
"github.com/docker/compose/v2/internal/registry" "github.com/docker/compose/v2/internal/registry"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
) )
func (s *composeService) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error { func (s *composeService) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.pull(ctx, project, options) return s.pull(ctx, project, options)
}, "pull", s.events) }, "pull", s.events)
} }
@ -67,9 +66,9 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
i := 0 i := 0
for name, service := range project.Services { for name, service := range project.Services {
if service.Image == "" { if service.Image == "" {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: name, ID: name,
Status: progress.Done, Status: api.Done,
Text: "Skipped", Text: "Skipped",
Details: "No image to be pulled", Details: "No image to be pulled",
}) })
@ -78,17 +77,17 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
switch service.PullPolicy { switch service.PullPolicy {
case types.PullPolicyNever, types.PullPolicyBuild: case types.PullPolicyNever, types.PullPolicyBuild:
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: "Image " + service.Image, ID: "Image " + service.Image,
Status: progress.Done, Status: api.Done,
Text: "Skipped", Text: "Skipped",
}) })
continue continue
case types.PullPolicyMissing, types.PullPolicyIfNotPresent: case types.PullPolicyMissing, types.PullPolicyIfNotPresent:
if imageAlreadyPresent(service.Image, images) { if imageAlreadyPresent(service.Image, images) {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: "Image " + service.Image, ID: "Image " + service.Image,
Status: progress.Done, Status: api.Done,
Text: "Skipped", Text: "Skipped",
Details: "Image is already present locally", Details: "Image is already present locally",
}) })
@ -97,9 +96,9 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
} }
if service.Build != nil && opts.IgnoreBuildable { if service.Build != nil && opts.IgnoreBuildable {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: "Image " + service.Image, ID: "Image " + service.Image,
Status: progress.Done, Status: api.Done,
Text: "Skipped", Text: "Skipped",
Details: "Image can be built", Details: "Image can be built",
}) })
@ -122,7 +121,7 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
} }
if !opts.IgnoreFailures && service.Build == nil { if !opts.IgnoreFailures && service.Build == nil {
if s.dryRun { if s.dryRun {
s.events.On(progress.ErrorEventf("Image "+service.Image, s.events.On(errorEventf("Image "+service.Image,
"error pulling image: %s", service.Image)) "error pulling image: %s", service.Image))
} }
// fail fast if image can't be pulled nor built // fail fast if image can't be pulled nor built
@ -174,7 +173,7 @@ func getUnwrappedErrorMessage(err error) string {
func (s *composeService) pullServiceImage(ctx context.Context, service types.ServiceConfig, quietPull bool, defaultPlatform string) (string, error) { func (s *composeService) pullServiceImage(ctx context.Context, service types.ServiceConfig, quietPull bool, defaultPlatform string) (string, error) {
resource := "Image " + service.Image resource := "Image " + service.Image
s.events.On(progress.PullingEvent(service.Image)) s.events.On(pullingEvent(service.Image))
ref, err := reference.ParseNormalizedNamed(service.Image) ref, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
return "", err return "", err
@ -196,9 +195,9 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
}) })
if ctx.Err() != nil { if ctx.Err() != nil {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: resource, ID: resource,
Status: progress.Warning, Status: api.Warning,
Text: "Interrupted", Text: "Interrupted",
}) })
return "", nil return "", nil
@ -207,16 +206,16 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
// check if has error and the service has a build section // check if has error and the service has a build section
// then the status should be warning instead of error // then the status should be warning instead of error
if err != nil && service.Build != nil { if err != nil && service.Build != nil {
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: resource, ID: resource,
Status: progress.Warning, Status: api.Warning,
Text: getUnwrappedErrorMessage(err), Text: getUnwrappedErrorMessage(err),
}) })
return "", err return "", err
} }
if err != nil { if err != nil {
s.events.On(progress.ErrorEvent(resource, getUnwrappedErrorMessage(err))) s.events.On(errorEvent(resource, getUnwrappedErrorMessage(err)))
return "", err return "", err
} }
@ -236,7 +235,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
toPullProgressEvent(resource, jm, s.events) toPullProgressEvent(resource, jm, s.events)
} }
} }
s.events.On(progress.PulledEvent(service.Image)) s.events.On(pulledEvent(service.Image))
inspected, err := s.apiClient().ImageInspect(ctx, service.Image) inspected, err := s.apiClient().ImageInspect(ctx, service.Image)
if err != nil { if err != nil {
@ -383,7 +382,7 @@ func isServiceImageToBuild(service types.ServiceConfig, services types.Services)
const ( const (
PreparingPhase = "Preparing" PreparingPhase = "Preparing"
WaitingPhase = "Waiting" WaitingPhase = "waiting"
PullingFsPhase = "Pulling fs layer" PullingFsPhase = "Pulling fs layer"
DownloadingPhase = "Downloading" DownloadingPhase = "Downloading"
DownloadCompletePhase = "Download complete" DownloadCompletePhase = "Download complete"
@ -393,7 +392,7 @@ const (
PullCompletePhase = "Pull complete" PullCompletePhase = "Pull complete"
) )
func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progress.EventProcessor) { func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events api.EventProcessor) {
if jm.ID == "" || jm.Progress == nil { if jm.ID == "" || jm.Progress == nil {
return return
} }
@ -403,7 +402,7 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progr
total int64 total int64
percent int percent int
current int64 current int64
status = progress.Working status = api.Working
) )
text = jm.Progress.String() text = jm.Progress.String()
@ -420,22 +419,22 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progr
} }
} }
case DownloadCompletePhase, AlreadyExistsPhase, PullCompletePhase: case DownloadCompletePhase, AlreadyExistsPhase, PullCompletePhase:
status = progress.Done status = api.Done
percent = 100 percent = 100
} }
if strings.Contains(jm.Status, "Image is up to date") || if strings.Contains(jm.Status, "Image is up to date") ||
strings.Contains(jm.Status, "Downloaded newer image") { strings.Contains(jm.Status, "Downloaded newer image") {
status = progress.Done status = api.Done
percent = 100 percent = 100
} }
if jm.Error != nil { if jm.Error != nil {
status = progress.Error status = api.Error
text = jm.Error.Message text = jm.Error.Message
} }
events.On(progress.Event{ events.On(api.Resource{
ID: jm.ID, ID: jm.ID,
ParentID: parent, ParentID: parent,
Current: current, Current: current,

View File

@ -33,14 +33,13 @@ import (
"github.com/docker/compose/v2/internal/registry" "github.com/docker/compose/v2/internal/registry"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
) )
func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error { func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
if options.Quiet { if options.Quiet {
return s.push(ctx, project, options) return s.push(ctx, project, options)
} }
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.push(ctx, project, options) return s.push(ctx, project, options)
}, "push", s.events) }, "push", s.events)
} }
@ -54,9 +53,9 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
if options.ImageMandatory && service.Image == "" && service.Provider == nil { if options.ImageMandatory && service.Image == "" && service.Provider == nil {
return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name) return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name)
} }
s.events.On(progress.Event{ s.events.On(api.Resource{
ID: service.Name, ID: service.Name,
Status: progress.Done, Status: api.Done,
Text: "Skipped", Text: "Skipped",
}) })
continue continue
@ -68,16 +67,16 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
for _, tag := range tags { for _, tag := range tags {
eg.Go(func() error { eg.Go(func() error {
s.events.On(progress.NewEvent(tag, progress.Working, "Pushing")) s.events.On(newEvent(tag, api.Working, "Pushing"))
err := s.pushServiceImage(ctx, tag, options.Quiet) err := s.pushServiceImage(ctx, tag, options.Quiet)
if err != nil { if err != nil {
if !options.IgnoreFailures { if !options.IgnoreFailures {
s.events.On(progress.NewEvent(tag, progress.Error, err.Error())) s.events.On(newEvent(tag, api.Error, err.Error()))
return err return err
} }
s.events.On(progress.NewEvent(tag, progress.Warning, err.Error())) s.events.On(newEvent(tag, api.Warning, err.Error()))
} else { } else {
s.events.On(progress.NewEvent(tag, progress.Done, "Pushed")) s.events.On(newEvent(tag, api.Done, "Pushed"))
} }
return nil return nil
}) })
@ -129,24 +128,24 @@ func (s *composeService) pushServiceImage(ctx context.Context, tag string, quiet
return nil return nil
} }
func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, events progress.EventProcessor) { func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, events api.EventProcessor) {
if jm.ID == "" { if jm.ID == "" {
// skipped // skipped
return return
} }
var ( var (
text string text string
status = progress.Working status = api.Working
total int64 total int64
current int64 current int64
percent int percent int
) )
if isDone(jm) { if isDone(jm) {
status = progress.Done status = api.Done
percent = 100 percent = 100
} }
if jm.Error != nil { if jm.Error != nil {
status = progress.Error status = api.Error
text = jm.Error.Message text = jm.Error.Message
} }
if jm.Progress != nil { if jm.Progress != nil {
@ -160,7 +159,7 @@ func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, events progr
} }
} }
events.On(progress.Event{ events.On(api.Resource{
ParentID: prefix, ParentID: prefix,
ID: jm.ID, ID: jm.ID,
Text: text, Text: text,

View File

@ -24,8 +24,6 @@ import (
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/docker/compose/v2/pkg/progress"
) )
func (s *composeService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error { func (s *composeService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error {
@ -76,8 +74,7 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options
}) })
if len(names) == 0 { if len(names) == 0 {
_, _ = fmt.Fprintln(s.stdinfo(), "No stopped containers") return api.ErrNoResources
return nil
} }
msg := fmt.Sprintf("Going to remove %s", strings.Join(names, ", ")) msg := fmt.Sprintf("Going to remove %s", strings.Join(names, ", "))
@ -92,7 +89,7 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options
return nil return nil
} }
} }
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.remove(ctx, stoppedContainers, options) return s.remove(ctx, stoppedContainers, options)
}, "remove", s.events) }, "remove", s.events)
} }
@ -102,13 +99,13 @@ func (s *composeService) remove(ctx context.Context, containers Containers, opti
for _, ctr := range containers { for _, ctr := range containers {
eg.Go(func() error { eg.Go(func() error {
eventName := getContainerProgressName(ctr) eventName := getContainerProgressName(ctr)
s.events.On(progress.RemovingEvent(eventName)) s.events.On(removingEvent(eventName))
err := s.apiClient().ContainerRemove(ctx, ctr.ID, container.RemoveOptions{ err := s.apiClient().ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
RemoveVolumes: options.Volumes, RemoveVolumes: options.Volumes,
Force: options.Force, Force: options.Force,
}) })
if err == nil { if err == nil {
s.events.On(progress.RemovedEvent(eventName)) s.events.On(removedEvent(eventName))
} }
return err return err
}) })

View File

@ -22,14 +22,13 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
func (s *composeService) Restart(ctx context.Context, projectName string, options api.RestartOptions) error { func (s *composeService) Restart(ctx context.Context, projectName string, options api.RestartOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.restart(ctx, strings.ToLower(projectName), options) return s.restart(ctx, strings.ToLower(projectName), options)
}, "restart", s.events) }, "restart", s.events)
} }
@ -93,13 +92,13 @@ func (s *composeService) restart(ctx context.Context, projectName string, option
} }
} }
eventName := getContainerProgressName(ctr) eventName := getContainerProgressName(ctr)
s.events.On(progress.RestartingEvent(eventName)) s.events.On(restartingEvent(eventName))
timeout := utils.DurationSecondToInt(options.Timeout) timeout := utils.DurationSecondToInt(options.Timeout)
err = s.apiClient().ContainerRestart(ctx, ctr.ID, container.StopOptions{Timeout: timeout}) err = s.apiClient().ContainerRestart(ctx, ctr.ID, container.StopOptions{Timeout: timeout})
if err != nil { if err != nil {
return err return err
} }
s.events.On(progress.StartedEvent(eventName)) s.events.On(startedEvent(eventName))
for _, hook := range def.PostStart { for _, hook := range def.PostStart {
err = s.runHook(ctx, ctr, def, hook, nil) err = s.runHook(ctx, ctr, def, hook, nil)
if err != nil { if err != nil {

View File

@ -28,7 +28,6 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
cmd "github.com/docker/cli/cli/command/container" cmd "github.com/docker/cli/cli/command/container"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
) )
@ -65,7 +64,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
return "", err return "", err
} }
err = progress.Run(ctx, func(ctx context.Context) error { err = Run(ctx, func(ctx context.Context) error {
return s.startDependencies(ctx, project, opts) return s.startDependencies(ctx, project, opts)
}, "run", s.events) }, "run", s.events)
if err != nil { if err != nil {

View File

@ -21,11 +21,10 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
) )
func (s *composeService) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error { func (s *composeService) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error {
return progress.Run(ctx, tracing.SpanWrapFunc("project/scale", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error { return Run(ctx, tracing.SpanWrapFunc("project/scale", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
err := s.create(ctx, project, api.CreateOptions{Services: options.Services}) err := s.create(ctx, project, api.CreateOptions{Services: options.Services})
if err != nil { if err != nil {
return err return err

View File

@ -23,7 +23,6 @@ import (
"strings" "strings"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
containerType "github.com/docker/docker/api/types/container" containerType "github.com/docker/docker/api/types/container"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
@ -31,7 +30,7 @@ import (
) )
func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error { func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.start(ctx, strings.ToLower(projectName), options, nil) return s.start(ctx, strings.ToLower(projectName), options, nil)
}, "start", s.events) }, "start", s.events)
} }

View File

@ -22,11 +22,10 @@ import (
"strings" "strings"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
) )
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error { func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
return progress.Run(ctx, func(ctx context.Context) error { return Run(ctx, func(ctx context.Context) error {
return s.stop(ctx, strings.ToLower(projectName), options, nil) return s.stop(ctx, strings.ToLower(projectName), options, nil)
}, "stop", s.events) }, "stop", s.events)
} }

View File

@ -33,14 +33,13 @@ import (
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
"github.com/eiannone/keyboard" "github.com/eiannone/keyboard"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo
err := progress.Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error { err := Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
err := s.create(ctx, project, options.Create) err := s.create(ctx, project, options.Create)
if err != nil { if err != nil {
return err return err
@ -126,7 +125,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
first := true first := true
gracefulTeardown := func() { gracefulTeardown := func() {
first = false first = false
fmt.Println("Gracefully Stopping... press Ctrl+C again to force") s.events.On(newEvent(api.ResourceCompose, api.Working, api.StatusStopping, "Gracefully Stopping... press Ctrl+C again to force"))
eg.Go(func() error { eg.Go(func() error {
err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{ err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{
Services: options.Create.Services, Services: options.Create.Services,
@ -162,7 +161,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
All: true, All: true,
}) })
// Ignore errors indicating that some of the containers were already stopped or removed. // Ignore errors indicating that some of the containers were already stopped or removed.
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) { if errdefs.IsNotFound(err) || errdefs.IsConflict(err) || errors.Is(err, api.ErrNoResources) {
return nil return nil
} }
@ -205,7 +204,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
} }
once = false once = false
exitCode = event.ExitCode exitCode = event.ExitCode
_, _ = fmt.Fprintln(s.stdinfo(), progress.ErrorColor("Aborting on container exit...")) s.events.On(newEvent(api.ResourceCompose, api.Working, api.StatusStopping, "Aborting on container exit..."))
eg.Go(func() error { eg.Go(func() error {
err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{ err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{
Services: options.Create.Services, Services: options.Create.Services,

View File

@ -33,9 +33,9 @@ import (
"github.com/docker/compose/v2/internal/sync" "github.com/docker/compose/v2/internal/sync"
"github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
cutils "github.com/docker/compose/v2/pkg/utils" cutils "github.com/docker/compose/v2/pkg/utils"
"github.com/docker/compose/v2/pkg/watch" "github.com/docker/compose/v2/pkg/watch"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/compose-spec/compose-go/v2/utils" "github.com/compose-spec/compose-go/v2/utils"
@ -472,7 +472,7 @@ func (t tarDockerClient) Exec(ctx context.Context, containerID string, cmd []str
}) })
} }
eg.Go(func() error { eg.Go(func() error {
_, err := io.Copy(t.s.stdinfo(), conn.Reader) _, err := io.Copy(t.s.stdout(), conn.Reader)
return err return err
}) })
@ -613,7 +613,7 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service(s) %q after changes were detected...", services)) options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service(s) %q after changes were detected...", services))
// restrict the build to ONLY this service, not any of its dependencies // restrict the build to ONLY this service, not any of its dependencies
options.Build.Services = services options.Build.Services = services
options.Build.Progress = progress.ModePlain options.Build.Progress = string(progressui.PlainMode)
options.Build.Out = cutils.GetWriter(func(line string) { options.Build.Out = cutils.GetWriter(func(line string) {
options.LogTo.Log(api.WatchLogger, line) options.LogTo.Log(api.WatchLogger, line)
}) })

View File

@ -1,234 +0,0 @@
/*
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 progress
import (
"context"
"fmt"
)
// EventStatus indicates the status of an action
type EventStatus int
const (
// Working means that the current task is working
Working EventStatus = iota
// Done means that the current task is done
Done
// Warning means that the current task has warning
Warning
// Error means that the current task has errored
Error
)
const (
StatusError = "Error"
StatusCreating = "Creating"
StatusStarting = "Starting"
StatusStarted = "Started"
StatusWaiting = "Waiting"
StatusHealthy = "Healthy"
StatusExited = "Exited"
StatusRestarting = "Restarting"
StatusRestarted = "Restarted"
StatusRunning = "Running"
StatusCreated = "Created"
StatusStopping = "Stopping"
StatusStopped = "Stopped"
StatusKilling = "Killing"
StatusKilled = "Killed"
StatusRemoving = "Removing"
StatusRemoved = "Removed"
StatusBuilding = "Building"
StatusBuilt = "Built"
StatusPulling = "Pulling"
StatusPulled = "Pulled"
StatusCommitting = "Committing"
StatusCommitted = "Committed"
StatusCopying = "Copying"
StatusCopied = "Copied"
StatusExporting = "Exporting"
StatusExported = "Exported"
)
// Event represents a progress event.
type Event struct {
ID string
ParentID string
Text string
Details string
Status EventStatus
Current int64
Percent int
Total int64
}
func (e *Event) StatusText() string {
switch e.Status {
case Working:
return "Working"
case Warning:
return "Warning"
case Done:
return "Done"
default:
return "Error"
}
}
// ErrorEvent creates a new Error Event with message
func ErrorEvent(id string, msg string) Event {
return Event{
ID: id,
Status: Error,
Text: StatusError,
Details: msg,
}
}
// ErrorEventf creates a new Error Event with format message
func ErrorEventf(id string, msg string, args ...any) Event {
return ErrorEvent(id, fmt.Sprintf(msg, args...))
}
// CreatingEvent creates a new Create in progress Event
func CreatingEvent(id string) Event {
return NewEvent(id, Working, StatusCreating)
}
// StartingEvent creates a new Starting in progress Event
func StartingEvent(id string) Event {
return NewEvent(id, Working, StatusStarting)
}
// StartedEvent creates a new Started in progress Event
func StartedEvent(id string) Event {
return NewEvent(id, Done, StatusStarted)
}
// Waiting creates a new waiting event
func Waiting(id string) Event {
return NewEvent(id, Working, StatusWaiting)
}
// Healthy creates a new healthy event
func Healthy(id string) Event {
return NewEvent(id, Done, StatusHealthy)
}
// Exited creates a new exited event
func Exited(id string) Event {
return NewEvent(id, Done, StatusExited)
}
// RestartingEvent creates a new Restarting in progress Event
func RestartingEvent(id string) Event {
return NewEvent(id, Working, StatusRestarting)
}
// RestartedEvent creates a new Restarted in progress Event
func RestartedEvent(id string) Event {
return NewEvent(id, Done, StatusRestarted)
}
// RunningEvent creates a new Running in progress Event
func RunningEvent(id string) Event {
return NewEvent(id, Done, StatusRunning)
}
// CreatedEvent creates a new Created (done) Event
func CreatedEvent(id string) Event {
return NewEvent(id, Done, StatusCreated)
}
// StoppingEvent creates a new Stopping in progress Event
func StoppingEvent(id string) Event {
return NewEvent(id, Working, StatusStopping)
}
// StoppedEvent creates a new Stopping in progress Event
func StoppedEvent(id string) Event {
return NewEvent(id, Done, StatusStopped)
}
// KillingEvent creates a new Killing in progress Event
func KillingEvent(id string) Event {
return NewEvent(id, Working, StatusKilling)
}
// KilledEvent creates a new Killed in progress Event
func KilledEvent(id string) Event {
return NewEvent(id, Done, StatusKilled)
}
// RemovingEvent creates a new Removing in progress Event
func RemovingEvent(id string) Event {
return NewEvent(id, Working, StatusRemoving)
}
// RemovedEvent creates a new removed (done) Event
func RemovedEvent(id string) Event {
return NewEvent(id, Done, StatusRemoved)
}
// BuildingEvent creates a new Building in progress Event
func BuildingEvent(id string) Event {
return NewEvent("Image "+id, Working, StatusBuilding)
}
// BuiltEvent creates a new built (done) Event
func BuiltEvent(id string) Event {
return NewEvent("Image "+id, Done, StatusBuilt)
}
// PullingEvent creates a new pulling (in progress) Event
func PullingEvent(id string) Event {
return NewEvent("Image "+id, Working, StatusPulling)
}
// PulledEvent creates a new pulled (done) Event
func PulledEvent(id string) Event {
return NewEvent("Image "+id, Done, StatusPulled)
}
// SkippedEvent creates a new Skipped Event
func SkippedEvent(id string, reason string) Event {
return Event{
ID: id,
Status: Warning,
Text: "Skipped: " + reason,
}
}
// NewEvent new event
func NewEvent(id string, status EventStatus, text string) Event {
return Event{
ID: id,
Status: status,
Text: text,
}
}
// EventProcessor is notified about Compose operations and tasks
type EventProcessor interface {
// Start is triggered as a Compose operation is starting with context
Start(ctx context.Context, operation string)
// On notify about (sub)task and progress processing operation
On(events ...Event)
// Done is triggered as a Compose operation completed
Done(operation string, success bool)
}