From aff5c115d6aca1faeba73560d8dafa39d545787a Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 5 Nov 2025 17:08:53 +0100 Subject: [PATCH] move progress UI components into cmd Signed-off-by: Nicolas De Loof --- cmd/compose/build.go | 10 +- cmd/compose/compose.go | 69 +++--- cmd/compose/kill.go | 9 +- cmd/compose/logs.go | 13 +- cmd/compose/options.go | 4 +- cmd/compose/remove.go | 9 +- cmd/compose/run.go | 4 +- cmd/compose/up.go | 4 +- {pkg/progress => cmd/display}/colors.go | 2 +- {pkg/progress => cmd/display}/json.go | 10 +- {pkg/progress => cmd/display}/json_test.go | 11 +- .../progress.go => cmd/display/mode.go | 21 +- {pkg/progress => cmd/display}/plain.go | 8 +- {pkg/progress => cmd/display}/quiet.go | 12 +- {pkg/progress => cmd/display}/spinner.go | 2 +- {pkg/progress => cmd/display}/tty.go | 61 +++-- pkg/api/errors.go | 12 +- pkg/api/event.go | 103 ++++++++ pkg/compose/build.go | 3 +- pkg/compose/build_bake.go | 19 +- pkg/compose/build_classic.go | 9 +- pkg/compose/commit.go | 19 +- pkg/compose/compose.go | 24 +- pkg/compose/convergence.go | 73 +++--- pkg/compose/cp.go | 15 +- pkg/compose/create.go | 17 +- pkg/compose/down.go | 45 ++-- pkg/compose/export.go | 17 +- pkg/compose/kill.go | 13 +- pkg/compose/model.go | 28 +-- pkg/compose/pause.go | 9 +- pkg/compose/plugins.go | 16 +- pkg/compose/progress.go | 176 +++++++++++++ pkg/compose/publish.go | 15 +- pkg/compose/pull.go | 49 ++-- pkg/compose/push.go | 25 +- pkg/compose/remove.go | 11 +- pkg/compose/restart.go | 7 +- pkg/compose/run.go | 3 +- pkg/compose/scale.go | 3 +- pkg/compose/start.go | 3 +- pkg/compose/stop.go | 3 +- pkg/compose/up.go | 9 +- pkg/compose/watch.go | 6 +- pkg/progress/event.go | 234 ------------------ 45 files changed, 619 insertions(+), 596 deletions(-) rename {pkg/progress => cmd/display}/colors.go (98%) rename {pkg/progress => cmd/display}/json.go (89%) rename {pkg/progress => cmd/display}/json_test.go (88%) rename pkg/progress/progress.go => cmd/display/mode.go (75%) rename {pkg/progress => cmd/display}/plain.go (87%) rename {pkg/progress => cmd/display}/quiet.go (83%) rename {pkg/progress => cmd/display}/spinner.go (98%) rename {pkg/progress => cmd/display}/tty.go (88%) create mode 100644 pkg/api/event.go create mode 100644 pkg/compose/progress.go delete mode 100644 pkg/progress/event.go diff --git a/cmd/compose/build.go b/cmd/compose/build.go index 3831c4b8f..5a86cc118 100644 --- a/cmd/compose/build.go +++ b/cmd/compose/build.go @@ -26,8 +26,8 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" cliopts "github.com/docker/cli/opts" + "github.com/docker/compose/v2/cmd/display" "github.com/docker/compose/v2/pkg/compose" - ui "github.com/docker/compose/v2/pkg/progress" "github.com/spf13/cobra" "github.com/docker/compose/v2/pkg/api" @@ -67,8 +67,8 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions, builderName = os.Getenv("BUILDX_BUILDER") } - uiMode := ui.Mode - if uiMode == ui.ModeJSON { + uiMode := display.Mode + if uiMode == display.ModeJSON { uiMode = "rawjson" } @@ -100,7 +100,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back Short: "Build or rebuild services", PreRunE: Adapt(func(ctx context.Context, args []string) error { if opts.quiet { - ui.Mode = ui.ModeQuiet + display.Mode = display.ModeQuiet devnull, err := os.Open(os.DevNull) if err != nil { 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 { if opts.print { - backendOptions.Add(compose.WithEventProcessor(ui.NewQuietWriter())) + backendOptions.Add(compose.WithEventProcessor(display.Quiet())) } backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...) if err != nil { diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 5ac4e1bbf..4e9e5f175 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -39,11 +39,11 @@ import ( "github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli/command" "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/internal/tracing" "github.com/docker/compose/v2/pkg/api" "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/utils" "github.com/morikuni/aec" @@ -84,10 +84,16 @@ func rawEnv(r io.Reader, filename string, vars map[string]string, lookup func(ke return nil } +var stdioToStdout bool + func init() { // compose evaluates env file values for interpolation // `raw` format allows to load env_file with the same parser used by docker run --env-file 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 @@ -116,7 +122,7 @@ func AdaptCmd(fn CobraCommand) func(cmd *cobra.Command, args []string) error { StatusCode: 130, } } - if ui.Mode == ui.ModeJSON { + if display.Mode == display.ModeJSON { err = makeJSONError(err) } return err @@ -486,49 +492,49 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C formatter.SetANSIMode(dockerCli, ansi) if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" { - ui.NoColor() + display.NoColor() formatter.SetANSIMode(dockerCli, formatter.Never) } switch ansi { case "never": - ui.Mode = ui.ModePlain + display.Mode = display.ModePlain case "always": - ui.Mode = ui.ModeTTY + display.Mode = display.ModeTTY } - var ep ui.EventProcessor + var ep api.EventProcessor switch opts.Progress { - case "", ui.ModeAuto: + case "", display.ModeAuto: switch { case ansi == "never": - ui.Mode = ui.ModePlain - ep = ui.NewPlainWriter(dockerCli.Err()) + display.Mode = display.ModePlain + ep = display.Plain(dockerCli.Err()) case dockerCli.Out().IsTerminal(): - ep = ui.NewTTYWriter(dockerCli.Err()) + ep = display.Full(dockerCli.Err(), stdinfo(dockerCli)) default: - ep = ui.NewPlainWriter(dockerCli.Err()) + ep = display.Plain(dockerCli.Err()) } - case ui.ModeTTY: + case display.ModeTTY: if ansi == "never" { return fmt.Errorf("can't use --progress tty while ANSI support is disabled") } - ui.Mode = ui.ModeTTY - ep = ui.NewTTYWriter(dockerCli.Err()) + display.Mode = display.ModeTTY + ep = display.Full(dockerCli.Err(), stdinfo(dockerCli)) - case ui.ModePlain: + case display.ModePlain: if ansi == "always" { return fmt.Errorf("can't use --progress plain while ANSI support is forced") } - ui.Mode = ui.ModePlain - ep = ui.NewPlainWriter(dockerCli.Err()) - case ui.ModeQuiet, "none": - ui.Mode = ui.ModeQuiet - ep = ui.NewQuietWriter() - case ui.ModeJSON: - ui.Mode = ui.ModeJSON + display.Mode = display.ModePlain + ep = display.Plain(dockerCli.Err()) + case display.ModeQuiet, "none": + display.Mode = display.ModeQuiet + ep = display.Quiet() + case display.ModeJSON: + display.Mode = display.ModeJSON logrus.SetFormatter(&logrus.JSONFormatter{}) - ep = ui.NewJSONWriter(dockerCli.Err()) + ep = display.JSON(dockerCli.Err()) default: return fmt.Errorf("unsupported --progress value %q", opts.Progress) } @@ -658,6 +664,13 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C return c } +func stdinfo(dockerCli command.Cli) io.Writer { + if stdioToStdout { + return dockerCli.Out() + } + return dockerCli.Err() +} + func setEnvWithDotEnv(opts ProjectOptions) error { options, err := cli.NewProjectOptions(opts.ConfigPaths, cli.WithWorkingDirectory(opts.ProjectDir), @@ -683,9 +696,9 @@ func setEnvWithDotEnv(opts ProjectOptions) error { } var printerModes = []string{ - ui.ModeAuto, - ui.ModeTTY, - ui.ModePlain, - ui.ModeJSON, - ui.ModeQuiet, + display.ModeAuto, + display.ModeTTY, + display.ModePlain, + display.ModeJSON, + display.ModeQuiet, } diff --git a/cmd/compose/kill.go b/cmd/compose/kill.go index 0bb96767d..5b8febc29 100644 --- a/cmd/compose/kill.go +++ b/cmd/compose/kill.go @@ -18,6 +18,8 @@ package compose import ( "context" + "errors" + "fmt" "os" "github.com/docker/cli/cli/command" @@ -65,10 +67,15 @@ func runKill(ctx context.Context, dockerCli command.Cli, backendOptions *Backend if err != nil { return err } - return backend.Kill(ctx, name, api.KillOptions{ + err = backend.Kill(ctx, name, api.KillOptions{ RemoveOrphans: opts.removeOrphans, Project: project, Services: services, Signal: opts.signal, }) + if errors.Is(err, api.ErrNoResources) { + _, _ = fmt.Fprintln(stdinfo(dockerCli), "No container to kill") + return nil + } + return err } diff --git a/cmd/compose/logs.go b/cmd/compose/logs.go index 4232c91fa..0a31f1f82 100644 --- a/cmd/compose/logs.go +++ b/cmd/compose/logs.go @@ -22,7 +22,6 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/pkg/compose" - "github.com/docker/compose/v2/pkg/progress" "github.com/spf13/cobra" "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{} type logConsumer struct { - events progress.EventProcessor + events api.EventProcessor } func (l logConsumer) Log(containerName, message string) { - l.events.On(progress.Event{ + l.events.On(api.Resource{ ID: containerName, Text: message, }) } func (l logConsumer) Err(containerName, message string) { - l.events.On(progress.Event{ + l.events.On(api.Resource{ ID: containerName, - Status: progress.Error, + Status: api.Error, Text: message, }) } func (l logConsumer) Status(containerName, message string) { - l.events.On(progress.Event{ + l.events.On(api.Resource{ ID: containerName, - Status: progress.Error, + Status: api.Error, Text: message, }) } diff --git a/cmd/compose/options.go b/cmd/compose/options.go index 2c864c58a..ff7ec1d29 100644 --- a/cmd/compose/options.go +++ b/cmd/compose/options.go @@ -30,9 +30,9 @@ import ( "github.com/compose-spec/compose-go/v2/template" "github.com/compose-spec/compose-go/v2/types" "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/internal/tracing" - ui "github.com/docker/compose/v2/pkg/progress" ) 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) { 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) } } diff --git a/cmd/compose/remove.go b/cmd/compose/remove.go index c8e713269..e3f2990db 100644 --- a/cmd/compose/remove.go +++ b/cmd/compose/remove.go @@ -18,6 +18,8 @@ package compose import ( "context" + "errors" + "fmt" "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/pkg/api" @@ -70,11 +72,16 @@ func runRemove(ctx context.Context, dockerCli command.Cli, backendOptions *Backe if err != nil { return err } - return backend.Remove(ctx, name, api.RemoveOptions{ + err = backend.Remove(ctx, name, api.RemoveOptions{ Services: services, Force: opts.force, Volumes: opts.volumes, Project: project, Stop: opts.stop, }) + if errors.Is(err, api.ErrNoResources) { + _, _ = fmt.Fprintln(stdinfo(dockerCli), "No stopped containers") + return nil + } + return err } diff --git a/cmd/compose/run.go b/cmd/compose/run.go index ab528837b..d0d4d9144 100644 --- a/cmd/compose/run.go +++ b/cmd/compose/run.go @@ -25,8 +25,8 @@ import ( composecli "github.com/compose-spec/compose-go/v2/cli" "github.com/compose-spec/compose-go/v2/dotenv" "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/progress" xprogress "github.com/moby/buildkit/util/progress/progressui" "github.com/sirupsen/logrus" @@ -193,7 +193,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen } if options.quiet { - progress.Mode = progress.ModeQuiet + display.Mode = display.ModeQuiet devnull, err := os.Open(os.DevNull) if err != nil { return err diff --git a/cmd/compose/up.go b/cmd/compose/up.go index f1dd9aa05..a65cab70a 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -26,8 +26,8 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" + "github.com/docker/compose/v2/cmd/display" "github.com/docker/compose/v2/pkg/compose" - ui "github.com/docker/compose/v2/pkg/progress" xprogress "github.com/moby/buildkit/util/progress/progressui" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -341,7 +341,7 @@ func runUp( WaitTimeout: timeout, Watch: upOptions.watch, Services: services, - NavigationMenu: upOptions.navigationMenu && ui.Mode != "plain" && dockerCli.In().IsTerminal(), + NavigationMenu: upOptions.navigationMenu && display.Mode != "plain" && dockerCli.In().IsTerminal(), }, }) } diff --git a/pkg/progress/colors.go b/cmd/display/colors.go similarity index 98% rename from pkg/progress/colors.go rename to cmd/display/colors.go index 716171df8..a00b4ed6a 100644 --- a/pkg/progress/colors.go +++ b/cmd/display/colors.go @@ -14,7 +14,7 @@ limitations under the License. */ -package progress +package display import ( "github.com/morikuni/aec" diff --git a/pkg/progress/json.go b/cmd/display/json.go similarity index 89% rename from pkg/progress/json.go rename to cmd/display/json.go index 8d48a8904..9f5946cd1 100644 --- a/pkg/progress/json.go +++ b/cmd/display/json.go @@ -14,16 +14,18 @@ limitations under the License. */ -package progress +package display import ( "context" "encoding/json" "fmt" "io" + + "github.com/docker/compose/v2/pkg/api" ) -func NewJSONWriter(out io.Writer) EventProcessor { +func JSON(out io.Writer) api.EventProcessor { return &jsonWriter{ out: out, } @@ -50,7 +52,7 @@ type jsonMessage struct { func (p *jsonWriter) Start(ctx context.Context, operation string) { } -func (p *jsonWriter) Event(e Event) { +func (p *jsonWriter) Event(e api.Resource) { message := &jsonMessage{ DryRun: p.dryRun, 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 { p.Event(e) } diff --git a/pkg/progress/json_test.go b/cmd/display/json_test.go similarity index 88% rename from pkg/progress/json_test.go rename to cmd/display/json_test.go index dd6d2c327..15d2180c2 100644 --- a/pkg/progress/json_test.go +++ b/cmd/display/json_test.go @@ -14,13 +14,14 @@ limitations under the License. */ -package progress +package display import ( "bytes" "encoding/json" "testing" + "github.com/docker/compose/v2/pkg/api" "gotest.tools/v3/assert" ) @@ -31,11 +32,11 @@ func TestJsonWriter_Event(t *testing.T) { dryRun: true, } - event := Event{ + event := api.Resource{ ID: "service1", ParentID: "project", - Status: Working, - Text: StatusCreating, + Status: api.Working, + Text: api.StatusCreating, Current: 50, Total: 100, Percent: 50, @@ -50,7 +51,7 @@ func TestJsonWriter_Event(t *testing.T) { DryRun: true, ID: event.ID, ParentID: event.ParentID, - Text: StatusCreating, + Text: api.StatusCreating, Status: "Working", Current: event.Current, Total: event.Total, diff --git a/pkg/progress/progress.go b/cmd/display/mode.go similarity index 75% rename from pkg/progress/progress.go rename to cmd/display/mode.go index e7cc9af5d..d66777b47 100644 --- a/pkg/progress/progress.go +++ b/cmd/display/mode.go @@ -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"); you may not use this file except in compliance with the License. @@ -14,20 +14,10 @@ limitations under the License. */ -package progress +package display -import ( - "context" -) - -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 -} +// Mode define how progress should be rendered, either as ModePlain or ModeTTY +var Mode = ModeAuto const ( // ModeAuto detect console capabilities @@ -41,6 +31,3 @@ const ( // ModeJSON outputs a machine-readable JSON stream ModeJSON = "json" ) - -// Mode define how progress should be rendered, either as ModePlain or ModeTTY -var Mode = ModeAuto diff --git a/pkg/progress/plain.go b/cmd/display/plain.go similarity index 87% rename from pkg/progress/plain.go rename to cmd/display/plain.go index 20132ad33..9af44a52a 100644 --- a/pkg/progress/plain.go +++ b/cmd/display/plain.go @@ -14,7 +14,7 @@ limitations under the License. */ -package progress +package display import ( "context" @@ -24,7 +24,7 @@ import ( "github.com/docker/compose/v2/pkg/api" ) -func NewPlainWriter(out io.Writer) EventProcessor { +func Plain(out io.Writer) api.EventProcessor { return &plainWriter{ out: out, } @@ -38,7 +38,7 @@ type plainWriter struct { func (p *plainWriter) Start(ctx context.Context, operation string) { } -func (p *plainWriter) Event(e Event) { +func (p *plainWriter) Event(e api.Resource) { prefix := "" if p.dryRun { 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) } -func (p *plainWriter) On(events ...Event) { +func (p *plainWriter) On(events ...api.Resource) { for _, e := range events { p.Event(e) } diff --git a/pkg/progress/quiet.go b/cmd/display/quiet.go similarity index 83% rename from pkg/progress/quiet.go rename to cmd/display/quiet.go index 31d146db9..c8459ae5b 100644 --- a/pkg/progress/quiet.go +++ b/cmd/display/quiet.go @@ -14,11 +14,15 @@ 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{} } @@ -30,5 +34,5 @@ func (q *quiet) Start(_ context.Context, _ string) { func (q *quiet) Done(_ string, _ bool) { } -func (q *quiet) On(_ ...Event) { +func (q *quiet) On(_ ...api.Resource) { } diff --git a/pkg/progress/spinner.go b/cmd/display/spinner.go similarity index 98% rename from pkg/progress/spinner.go rename to cmd/display/spinner.go index 4e434bd4f..e476deae8 100644 --- a/pkg/progress/spinner.go +++ b/cmd/display/spinner.go @@ -14,7 +14,7 @@ limitations under the License. */ -package progress +package display import ( "runtime" diff --git a/pkg/progress/tty.go b/cmd/display/tty.go similarity index 88% rename from pkg/progress/tty.go rename to cmd/display/tty.go index 7e3d1f66b..228664502 100644 --- a/pkg/progress/tty.go +++ b/cmd/display/tty.go @@ -14,7 +14,7 @@ limitations under the License. */ -package progress +package display import ( "context" @@ -31,11 +31,12 @@ import ( "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 -func NewTTYWriter(out io.Writer) EventProcessor { +func Full(out io.Writer, info io.Writer) api.EventProcessor { return &ttyWriter{ out: out, + info: info, tasks: map[string]task{}, done: make(chan bool), mtx: &sync.Mutex{}, @@ -55,6 +56,7 @@ type ttyWriter struct { operation string ticker *time.Ticker suspended bool + info io.Writer } type task struct { @@ -64,7 +66,7 @@ type task struct { endTime time.Time text string details string - status EventStatus + status api.EventStatus current int64 percent int total int64 @@ -108,11 +110,16 @@ func (w *ttyWriter) Done(operation string, success bool) { w.done <- true } -func (w *ttyWriter) On(events ...Event) { +func (w *ttyWriter) On(events ...api.Resource) { w.mtx.Lock() defer w.mtx.Unlock() 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 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 - if e.Text == StatusBuilding { + if e.Text == api.StatusBuilding { w.ticker.Stop() w.suspended = true } else if w.suspended { @@ -132,11 +139,11 @@ func (w *ttyWriter) event(e Event) { if last, ok := w.tasks[e.ID]; ok { switch e.Status { - case Done, Error, Warning: + case api.Done, api.Error, api.Warning: if last.status != e.Status { last.stop() } - case Working: + case api.Working: last.hasMore() } last.status = e.Status @@ -170,7 +177,7 @@ func (w *ttyWriter) event(e Event) { total: e.Total, spinner: NewSpinner(), } - if e.Status == Done || e.Status == Error { + if e.Status == api.Done || e.Status == api.Error { t.stop() } w.tasks[e.ID] = t @@ -179,7 +186,7 @@ func (w *ttyWriter) event(e Event) { w.printEvent(e) } -func (w *ttyWriter) printEvent(e Event) { +func (w *ttyWriter) printEvent(e api.Resource) { if w.operation != "" { // event will be displayed by progress UI on ticker's ticks return @@ -187,13 +194,13 @@ func (w *ttyWriter) printEvent(e Event) { var color colorFunc switch e.Status { - case Working: + case api.Working: color = SuccessColor - case Done: + case api.Done: color = SuccessColor - case Warning: + case api.Warning: color = WarningColor - case Error: + case api.Error: color = ErrorColor } _, _ = 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 { endTime := time.Now() - if t.status != Working { + if t.status != api.Working { endTime = t.startTime if (t.endTime != time.Time{}) { 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 - if parent := t; parent.status == Working { + if parent := t; parent.status == api.Working { for _, id := range w.ids { child := w.tasks[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 // so don't show the total progress yet hideDetails = true @@ -361,24 +368,24 @@ var ( func spinner(t task) string { switch t.status { - case Done: + case api.Done: return SuccessColor(spinnerDone) - case Warning: + case api.Warning: return WarningColor(spinnerWarning) - case Error: + case api.Error: return ErrorColor(spinnerError) default: return CountColor(t.spinner.String()) } } -func colorFn(s EventStatus) colorFunc { +func colorFn(s api.EventStatus) colorFunc { switch s { - case Done: + case api.Done: return SuccessColor - case Warning: + case api.Warning: return WarningColor - case Error: + case api.Error: return ErrorColor default: return nocolor @@ -388,7 +395,7 @@ func colorFn(s EventStatus) colorFunc { func numDone(tasks map[string]task) int { i := 0 for _, t := range tasks { - if t.status != Working { + if t.status != api.Working { i++ } } diff --git a/pkg/api/errors.go b/pkg/api/errors.go index c61c2d0f4..7bf4d4a02 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors.go @@ -35,12 +35,7 @@ var ( ErrForbidden = errors.New("forbidden") // ErrUnknown is returned when the error type is unmapped ErrUnknown = errors.New("unknown") - // ErrLoginFailed is returned when login failed - 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 is returned when a backend doesn't implement an action ErrNotImplemented = errors.New("not implemented") // ErrUnsupportedFlag is returned when a backend doesn't support a flag ErrUnsupportedFlag = errors.New("unsupported flag") @@ -48,9 +43,8 @@ var ( ErrCanceled = errors.New("canceled") // ErrParsingFailed is returned when a string cannot be parsed ErrParsingFailed = errors.New("parsing failed") - // ErrWrongContextType is returned when the caller tries to get a context - // with the wrong type - ErrWrongContextType = errors.New("wrong context type") + // ErrNoResources is returned when operation didn't selected any resource + ErrNoResources = errors.New("no resources") ) // IsNotFoundError returns true if the unwrapped error is ErrNotFound diff --git a/pkg/api/event.go b/pkg/api/event.go new file mode 100644 index 000000000..8a14fa316 --- /dev/null +++ b/pkg/api/event.go @@ -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) +} diff --git a/pkg/compose/build.go b/pkg/compose/build.go index c832ff148..d6edb2653 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -26,7 +26,6 @@ import ( "github.com/containerd/platforms" "github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/utils" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" @@ -37,7 +36,7 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti if err != nil { 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), func(ctx context.Context) error { _, err := s.build(ctx, project, options, nil) diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 83688d6fa..cccf7a3c9 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -40,7 +40,6 @@ import ( "github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/cli/streams" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/api/types/versions" "github.com/google/uuid" "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 eg := errgroup.Group{} 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") } - displayMode := progressui.DisplayMode(options.Progress) out := options.Out if out == nil { out = s.stdout() @@ -206,7 +205,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project } image := api.GetImageNameOrDefault(service, project.Name) - s.events.On(progress.BuildingEvent(image)) + s.events.On(buildingEvent(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) } results[image] = built.Digest - s.events.On(progress.BuiltEvent(image)) + s.events.On(builtEvent(image)) } return results, nil } @@ -554,20 +553,20 @@ func (s composeService) dryRunBake(cfg bakeConfig) map[string]string { bakeResponse[name] = dryRunUUID } for name := range bakeResponse { - s.events.On(progress.BuiltEvent(name)) + s.events.On(builtEvent(name)) } return bakeResponse } func (s composeService) displayDryRunBuildEvent(name, dryRunUUID, tag string) { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name + " ==>", - Status: progress.Done, + Status: api.Done, Text: fmt.Sprintf("==> writing image %s", dryRunUUID), }) - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name + " ==> ==>", - Status: progress.Done, + Status: api.Done, Text: fmt.Sprintf(`naming to %s`, tag), }) } diff --git a/pkg/compose/build_classic.go b/pkg/compose/build_classic.go index 845ea120a..9161dc201 100644 --- a/pkg/compose/build_classic.go +++ b/pkg/compose/build_classic.go @@ -30,7 +30,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command/image/build" "github.com/docker/compose/v2/pkg/api" - progress2 "github.com/docker/compose/v2/pkg/progress" buildtypes "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/container" "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) - s.events.On(progress2.BuildingEvent(image)) + s.events.On(buildingEvent(image)) id, err := s.doBuildImage(ctx, project, service, options) if err != nil { return err } - s.events.On(progress2.BuiltEvent(image)) + s.events.On(builtEvent(image)) builtDigests[getServiceIndex(name)] = id if options.Push { @@ -258,7 +257,7 @@ func (s *composeService) doBuildImage(ctx context.Context, project *types.Projec ctx, cancel := context.WithCancel(ctx) defer cancel() - s.events.On(progress2.BuildingEvent(imageName)) + s.events.On(buildingEvent(imageName)) response, err := s.apiClient().ImageBuild(ctx, body, buildOpts) if err != nil { return "", err @@ -287,7 +286,7 @@ func (s *composeService) doBuildImage(ctx context.Context, project *types.Projec } return "", err } - s.events.On(progress2.BuiltEvent(imageName)) + s.events.On(builtEvent(imageName)) return imageID, nil } diff --git a/pkg/compose/commit.go b/pkg/compose/commit.go index f24d85e1c..db69a06bb 100644 --- a/pkg/compose/commit.go +++ b/pkg/compose/commit.go @@ -22,12 +22,11 @@ import ( "strings" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/api/types/container" ) 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) }, "commit", s.events) } @@ -42,17 +41,17 @@ func (s *composeService) commit(ctx context.Context, projectName string, options name := getCanonicalContainerName(ctr) - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name, - Status: progress.Working, - Text: progress.StatusCommitting, + Status: api.Working, + Text: api.StatusCommitting, }) if s.dryRun { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name, - Status: progress.Done, - Text: progress.StatusCommitted, + Status: api.Done, + Text: api.StatusCommitted, }) return nil @@ -69,10 +68,10 @@ func (s *composeService) commit(ctx context.Context, projectName string, options return err } - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name, Text: fmt.Sprintf("Committed as %s", response.ID), - Status: progress.Done, + Status: api.Done, }) return nil diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index b660c02b5..a54c32c15 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" "io" - "os" "strconv" "strings" "sync" @@ -32,7 +31,6 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/flags" "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/filters" "github.com/docker/docker/api/types/network" @@ -45,15 +43,6 @@ import ( "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 // 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 { - s.events = progress.NewQuietWriter() + s.events = &ignore{} } // 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. // Typically used to configure a progress UI -func WithEventProcessor(bus progress.EventProcessor) Option { +func WithEventProcessor(bus api.EventProcessor) Option { return func(s *composeService) error { s.events = bus return nil @@ -216,7 +205,7 @@ type composeService struct { // prompt is used to interact with user and confirm actions prompt Prompt // eventBus collects tasks execution events - events progress.EventProcessor + events api.EventProcessor // Optional overrides for specific components (for SDK users) outStream io.Writer @@ -278,13 +267,6 @@ func (s *composeService) stderr() *streams.Out { 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 type readCloserAdapter struct { r io.Reader diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 952e468c4..245c1ed81 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -41,7 +41,6 @@ import ( "github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/utils" ) @@ -187,7 +186,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project, name := getContainerProgressName(ctr) switch ctr.State { case container.StateRunning: - c.compose.events.On(progress.RunningEvent(name)) + c.compose.events.On(runningEvent(name)) case container.StateCreated: case container.StateRestarting: case container.StateExited: @@ -426,16 +425,16 @@ func getContainerProgressName(ctr container.Summary) string { return "Container " + getCanonicalContainerName(ctr) } -func containerEvents(containers Containers, eventFunc func(string) progress.Event) []progress.Event { - events := []progress.Event{} +func containerEvents(containers Containers, eventFunc func(string) api.Resource) []api.Resource { + events := []api.Resource{} for _, ctr := range containers { events = append(events, eventFunc(getContainerProgressName(ctr))) } return events } -func containerReasonEvents(containers Containers, eventFunc func(string, string) progress.Event, reason string) []progress.Event { - events := []progress.Event{} +func containerReasonEvents(containers Containers, eventFunc func(string, string) api.Resource, reason string) []api.Resource { + events := []api.Resource{} for _, ctr := range containers { 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) - s.events.On(containerEvents(waitingFor, progress.Waiting)...) + s.events.On(containerEvents(waitingFor, waiting)...) if len(waitingFor) == 0 { if config.Required { 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 { case ServiceConditionRunningOrHealthy: - healthy, err := s.isServiceHealthy(ctx, waitingFor, true) + isHealthy, err := s.isServiceHealthy(ctx, waitingFor, true) if err != nil { 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))...) logrus.Warnf("optional dependency %q is not running or is unhealthy: %s", dep, err.Error()) return nil } return err } - if healthy { - s.events.On(containerEvents(waitingFor, progress.Healthy)...) + if isHealthy { + s.events.On(containerEvents(waitingFor, healthy)...) return nil } case types.ServiceConditionHealthy: - healthy, err := s.isServiceHealthy(ctx, waitingFor, false) + isHealthy, err := s.isServiceHealthy(ctx, waitingFor, false) if err != nil { 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))...) logrus.Warnf("optional dependency %q failed to start: %s", dep, err.Error()) return nil } - s.events.On(containerEvents(waitingFor, func(s string) progress.Event { - return progress.ErrorEventf(s, "dependency %s failed to start", dep) + s.events.On(containerEvents(waitingFor, func(s string) api.Resource { + return errorEventf(s, "dependency %s failed to start", dep) })...) return fmt.Errorf("dependency failed to start: %w", err) } - if healthy { - s.events.On(containerEvents(waitingFor, progress.Healthy)...) + if isHealthy { + s.events.On(containerEvents(waitingFor, healthy)...) return nil } case types.ServiceConditionCompletedSuccessfully: - exited, code, err := s.isServiceCompleted(ctx, waitingFor) + isExited, code, err := s.isServiceCompleted(ctx, waitingFor) if err != nil { return err } - if exited { + if isExited { if code == 0 { - s.events.On(containerEvents(waitingFor, progress.Exited)...) + s.events.On(containerEvents(waitingFor, exited)...) return nil } messageSuffix := fmt.Sprintf("%q didn't complete successfully: exit %d", dep, code) if !config.Required { // 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))...) logrus.Warnf("optional dependency %s", messageSuffix) return nil } msg := fmt.Sprintf("service %s", messageSuffix) - s.events.On(containerEvents(waitingFor, func(s string) progress.Event { - return progress.ErrorEventf(s, "service %s", messageSuffix) + s.events.On(containerEvents(waitingFor, func(s string) api.Resource { + return errorEventf(s, "service %s", messageSuffix) })...) return errors.New(msg) } @@ -599,19 +598,19 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro name string, number int, opts createOptions, ) (ctr container.Summary, err error) { 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) if err != nil { if ctx.Err() == nil { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: eventName, - Status: progress.Error, + Status: api.Error, Text: err.Error(), }) } return ctr, err } - s.events.On(progress.CreatedEvent(eventName)) + s.events.On(createdEvent(eventName)) 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, ) (created container.Summary, err error) { eventName := getContainerProgressName(replaced) - s.events.On(progress.NewEvent(eventName, progress.Working, "Recreate")) + s.events.On(newEvent(eventName, api.Working, "Recreate")) defer func() { if err != nil && ctx.Err() == nil { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: eventName, - Status: progress.Error, + Status: api.Error, Text: err.Error(), }) } @@ -673,7 +672,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P return created, err } - s.events.On(progress.NewEvent(eventName, progress.Done, "Recreated")) + s.events.On(newEvent(eventName, api.Done, "Recreated")) return created, err } @@ -681,14 +680,14 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P var startMx sync.Mutex 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() defer startMx.Unlock() err := s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{}) if err != nil { return err } - s.events.On(progress.NewEvent(getContainerProgressName(ctr), progress.Done, "Restarted")) + s.events.On(newEvent(getContainerProgressName(ctr), api.Done, "Restarted")) return nil } @@ -719,9 +718,9 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types return created, err } for _, warning := range response.Warnings { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: service.Name, - Status: progress.Warning, + Status: api.Warning, Text: warning, }) } @@ -906,7 +905,7 @@ func (s *composeService) startService(ctx context.Context, } eventName := getContainerProgressName(ctr) - s.events.On(progress.StartingEvent(eventName)) + s.events.On(startingEvent(eventName)) err = s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{}) if err != nil { 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 } diff --git a/pkg/compose/cp.go b/pkg/compose/cp.go index dcd03f2b1..0aafc1db0 100644 --- a/pkg/compose/cp.go +++ b/pkg/compose/cp.go @@ -25,7 +25,6 @@ import ( "path/filepath" "strings" - "github.com/docker/compose/v2/pkg/progress" "golang.org/x/sync/errgroup" "github.com/docker/cli/cli/command" @@ -43,7 +42,7 @@ const ( ) 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) }, "copy", s.events) } @@ -90,20 +89,20 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a } else { msg = fmt.Sprintf("%s to %s:%s", srcPath, name, dstPath) } - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name, - Text: progress.StatusCopying, + Text: api.StatusCopying, Details: msg, - Status: progress.Working, + Status: api.Working, }) if err := copyFunc(ctx, ctr.ID, srcPath, dstPath, options); err != nil { return err } - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name, - Text: progress.StatusCopied, + Text: api.StatusCopied, Details: msg, - Status: progress.Done, + Status: api.Done, }) return nil }) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 495a0271b..1aabedb76 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -43,7 +43,6 @@ import ( cdi "tags.cncf.io/container-device-interface/pkg/parser" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" ) type createOptions struct { @@ -61,7 +60,7 @@ type createConfigs struct { } 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) }, "create", s.events) } @@ -1394,14 +1393,14 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty } 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) 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) } - s.events.On(progress.CreatedEvent(networkEventName)) + s.events.On(createdEvent(networkEventName)) err = s.connectNetwork(ctx, n.Name, dangledContainers, nil) if err != nil { @@ -1443,7 +1442,7 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ err = s.apiClient().NetworkRemove(ctx, n.Name) eventName := fmt.Sprintf("Network %s", n.Name) - s.events.On(progress.RemovedEvent(eventName)) + s.events.On(removedEvent(eventName)) 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 { eventName := fmt.Sprintf("Volume %s", volume.Name) - s.events.On(progress.CreatingEvent(eventName)) + s.events.On(creatingEvent(eventName)) hash, err := VolumeHash(volume) if err != nil { return err @@ -1632,9 +1631,9 @@ func (s *composeService) createVolume(ctx context.Context, volume types.VolumeCo DriverOpts: volume.DriverOpts, }) if err != nil { - s.events.On(progress.ErrorEvent(eventName, err.Error())) + s.events.On(errorEvent(eventName, err.Error())) return err } - s.events.On(progress.CreatedEvent(eventName)) + s.events.On(createdEvent(eventName)) return nil } diff --git a/pkg/compose/down.go b/pkg/compose/down.go index 1136faf5d..3572c09d6 100644 --- a/pkg/compose/down.go +++ b/pkg/compose/down.go @@ -25,7 +25,6 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/containerd/errdefs" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/utils" containerType "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -38,7 +37,7 @@ import ( type downOp func() 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) }, "down", s.events) } @@ -210,7 +209,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s } eventName := fmt.Sprintf("Network %s", name) - s.events.On(progress.RemovingEvent(eventName)) + s.events.On(removingEvent(eventName)) var found int 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{}) 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 } if err != nil { return err } 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++ continue } @@ -235,10 +234,10 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s if errdefs.IsNotFound(err) { 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) } - s.events.On(progress.RemovedEvent(eventName)) + s.events.On(removedEvent(eventName)) 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 // mean the network was present when we queried at the start of this // 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 @@ -254,18 +253,18 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s func (s *composeService) removeImage(ctx context.Context, image string) error { 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{}) if err == nil { - s.events.On(progress.NewEvent(id, progress.Done, "Removed")) + s.events.On(newEvent(id, api.Done, "Removed")) return nil } 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 } 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 err @@ -280,18 +279,18 @@ func (s *composeService) removeVolume(ctx context.Context, id string) error { 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) if err == nil { - s.events.On(progress.NewEvent(resource, progress.Done, "Removed")) + s.events.On(newEvent(resource, api.Done, "Removed")) return nil } 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 } 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 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 { eventName := getContainerProgressName(ctr) - s.events.On(progress.StoppingEvent(eventName)) + s.events.On(stoppingEvent(eventName)) if service != nil { for _, hook := range service.PreStop { @@ -317,10 +316,10 @@ func (s *composeService) stopContainer(ctx context.Context, service *types.Servi timeoutInSecond := utils.DurationSecondToInt(timeout) err := s.apiClient().ContainerStop(ctx, ctr.ID, containerType.StopOptions{Timeout: timeoutInSecond}) if err != nil { - s.events.On(progress.ErrorEvent(eventName, "Error while Stopping")) + s.events.On(errorEvent(eventName, "Error while Stopping")) return err } - s.events.On(progress.StoppedEvent(eventName)) + s.events.On(stoppedEvent(eventName)) return nil } @@ -348,22 +347,22 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain eventName := getContainerProgressName(ctr) err := s.stopContainer(ctx, service, ctr, timeout, nil) if errdefs.IsNotFound(err) { - s.events.On(progress.RemovedEvent(eventName)) + s.events.On(removedEvent(eventName)) return nil } if err != nil { return err } - s.events.On(progress.RemovingEvent(eventName)) + s.events.On(removingEvent(eventName)) err = s.apiClient().ContainerRemove(ctx, ctr.ID, containerType.RemoveOptions{ Force: true, RemoveVolumes: volumes, }) 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 } - s.events.On(progress.RemovedEvent(eventName)) + s.events.On(removedEvent(eventName)) return nil } diff --git a/pkg/compose/export.go b/pkg/compose/export.go index c2c02d18f..28af3261d 100644 --- a/pkg/compose/export.go +++ b/pkg/compose/export.go @@ -24,12 +24,11 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/moby/sys/atomicwriter" ) 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) }, "export", s.events) } @@ -51,10 +50,10 @@ func (s *composeService) export(ctx context.Context, projectName string, options } name := getCanonicalContainerName(container) - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name, - Text: progress.StatusExporting, - Status: progress.Working, + Text: api.StatusExporting, + Status: api.Working, }) responseBody, err := s.apiClient().ContainerExport(ctx, container.ID) @@ -64,7 +63,7 @@ func (s *composeService) export(ctx context.Context, projectName string, options defer func() { 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, - Text: progress.StatusExported, - Status: progress.Done, + Text: api.StatusExported, + Status: api.Done, }) return nil diff --git a/pkg/compose/kill.go b/pkg/compose/kill.go index b730c6166..c25838579 100644 --- a/pkg/compose/kill.go +++ b/pkg/compose/kill.go @@ -18,18 +18,16 @@ package compose import ( "context" - "fmt" "strings" "github.com/docker/docker/api/types/container" "golang.org/x/sync/errgroup" "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 { - 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) }, "kill", s.events) } @@ -55,21 +53,20 @@ func (s *composeService) kill(ctx context.Context, projectName string, options a containers = containers.filter(isService(project.ServiceNames()...)) } if len(containers) == 0 { - _, _ = fmt.Fprintf(s.stdinfo(), "no container to kill") - return nil + return api.ErrNoResources } eg, ctx := errgroup.WithContext(ctx) containers.forEach(func(ctr container.Summary) { eg.Go(func() error { eventName := getContainerProgressName(ctr) - s.events.On(progress.KillingEvent(eventName)) + s.events.On(killingEvent(eventName)) err := s.apiClient().ContainerKill(ctx, ctr.ID, options.Signal) if err != nil { - s.events.On(progress.ErrorEvent(eventName, "Error while Killing")) + s.events.On(errorEvent(eventName, "Error while Killing")) return err } - s.events.On(progress.KilledEvent(eventName)) + s.events.On(killedEvent(eventName)) return nil }) }) diff --git a/pkg/compose/model.go b/pkg/compose/model.go index 6a9363a48..7e6f36a64 100644 --- a/pkg/compose/model.go +++ b/pkg/compose/model.go @@ -29,7 +29,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/containerd/errdefs" "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" "golang.org/x/sync/errgroup" ) @@ -101,10 +101,10 @@ func (m *modelAPI) Close() { m.cleanup() } -func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, events progress.EventProcessor) error { - events.On(progress.Event{ +func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, events api.EventProcessor) error { + events.On(api.Resource{ ID: model.Name, - Status: progress.Working, + Status: api.Working, Text: "Pulling", }) @@ -131,30 +131,30 @@ func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quiet } if !quietPull { - events.On(progress.Event{ + events.On(api.Resource{ ID: model.Name, - Status: progress.Working, - Text: progress.StatusPulling, + Status: api.Working, + Text: api.StatusPulling, }) } } err = cmd.Wait() 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, - Status: progress.Working, - Text: progress.StatusPulled, + Status: api.Working, + Text: api.StatusPulled, }) return err } -func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events progress.EventProcessor) error { - events.On(progress.Event{ +func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events api.EventProcessor) error { + events.On(api.Resource{ ID: config.Name, - Status: progress.Working, + Status: api.Working, Text: "Configuring", }) // configure [--context-size=] MODEL [-- ] diff --git a/pkg/compose/pause.go b/pkg/compose/pause.go index cfa5d2c77..882dcb660 100644 --- a/pkg/compose/pause.go +++ b/pkg/compose/pause.go @@ -24,11 +24,10 @@ import ( "golang.org/x/sync/errgroup" "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 { - 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) }, "pause", s.events) } @@ -49,7 +48,7 @@ func (s *composeService) pause(ctx context.Context, projectName string, options err := s.apiClient().ContainerPause(ctx, container.ID) if err == nil { eventName := getContainerProgressName(container) - s.events.On(progress.NewEvent(eventName, progress.Done, "Paused")) + s.events.On(newEvent(eventName, api.Done, "Paused")) } 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 { - 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) }, "unpause", s.events) } @@ -79,7 +78,7 @@ func (s *composeService) unPause(ctx context.Context, projectName string, option err = s.apiClient().ContainerUnpause(ctx, ctr.ID) if err == nil { eventName := getContainerProgressName(ctr) - s.events.On(progress.NewEvent(eventName, progress.Done, "Unpaused")) + s.events.On(newEvent(eventName, api.Done, "Unpaused")) } return err }) diff --git a/pkg/compose/plugins.go b/pkg/compose/plugins.go index 97bb604e9..e872528fb 100644 --- a/pkg/compose/plugins.go +++ b/pkg/compose/plugins.go @@ -33,7 +33,7 @@ import ( "github.com/containerd/errdefs" "github.com/docker/cli/cli-plugins/manager" "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/spf13/cobra" ) @@ -89,10 +89,10 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty var action string switch command { case "up": - s.events.On(progress.CreatingEvent(service.Name)) + s.events.On(creatingEvent(service.Name)) action = "create" case "down": - s.events.On(progress.RemovingEvent(service.Name)) + s.events.On(removingEvent(service.Name)) action = "remove" default: 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 { 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) 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: key, val, found := strings.Cut(msg.Message, "=") if !found { @@ -143,14 +143,14 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty err = cmd.Wait() 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()) } switch command { case "up": - s.events.On(progress.CreatedEvent(service.Name)) + s.events.On(createdEvent(service.Name)) case "down": - s.events.On(progress.RemovedEvent(service.Name)) + s.events.On(removedEvent(service.Name)) } return variables, nil } diff --git a/pkg/compose/progress.go b/pkg/compose/progress.go new file mode 100644 index 000000000..a958c83a8 --- /dev/null +++ b/pkg/compose/progress.go @@ -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) { +} diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 5d7b0fe7b..4e084eb2a 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -35,7 +35,6 @@ import ( "github.com/docker/compose/v2/internal/oci" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/compose/transform" - "github.com/docker/compose/v2/pkg/progress" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" 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 { - return progress.Run(ctx, func(ctx context.Context) error { + return Run(ctx, func(ctx context.Context) error { return s.publish(ctx, project, repository, options) }, "publish", s.events) } @@ -71,10 +70,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re return err } - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: repository, Text: "publishing", - Status: progress.Working, + Status: api.Working, }) if logrus.IsLevelEnabled(logrus.DebugLevel) { 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) if err != nil { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: repository, Text: "publishing", - Status: progress.Error, + Status: api.Error, }) 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, Text: "published", - Status: progress.Done, + Status: api.Done, }) return nil } diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index 1906f0521..e9a6385e1 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -40,11 +40,10 @@ import ( "github.com/docker/compose/v2/internal/registry" "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 { - return progress.Run(ctx, func(ctx context.Context) error { + return Run(ctx, func(ctx context.Context) error { return s.pull(ctx, project, options) }, "pull", s.events) } @@ -67,9 +66,9 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts i := 0 for name, service := range project.Services { if service.Image == "" { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: name, - Status: progress.Done, + Status: api.Done, Text: "Skipped", Details: "No image to be pulled", }) @@ -78,17 +77,17 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts switch service.PullPolicy { case types.PullPolicyNever, types.PullPolicyBuild: - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: "Image " + service.Image, - Status: progress.Done, + Status: api.Done, Text: "Skipped", }) continue case types.PullPolicyMissing, types.PullPolicyIfNotPresent: if imageAlreadyPresent(service.Image, images) { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: "Image " + service.Image, - Status: progress.Done, + Status: api.Done, Text: "Skipped", 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 { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: "Image " + service.Image, - Status: progress.Done, + Status: api.Done, Text: "Skipped", 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 s.dryRun { - s.events.On(progress.ErrorEventf("Image "+service.Image, + s.events.On(errorEventf("Image "+service.Image, "error pulling image: %s", service.Image)) } // 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) { resource := "Image " + service.Image - s.events.On(progress.PullingEvent(service.Image)) + s.events.On(pullingEvent(service.Image)) ref, err := reference.ParseNormalizedNamed(service.Image) if err != nil { return "", err @@ -196,9 +195,9 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser }) if ctx.Err() != nil { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: resource, - Status: progress.Warning, + Status: api.Warning, Text: "Interrupted", }) 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 // then the status should be warning instead of error if err != nil && service.Build != nil { - s.events.On(progress.Event{ + s.events.On(api.Resource{ ID: resource, - Status: progress.Warning, + Status: api.Warning, Text: getUnwrappedErrorMessage(err), }) return "", err } if err != nil { - s.events.On(progress.ErrorEvent(resource, getUnwrappedErrorMessage(err))) + s.events.On(errorEvent(resource, getUnwrappedErrorMessage(err))) return "", err } @@ -236,7 +235,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser 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) if err != nil { @@ -383,7 +382,7 @@ func isServiceImageToBuild(service types.ServiceConfig, services types.Services) const ( PreparingPhase = "Preparing" - WaitingPhase = "Waiting" + WaitingPhase = "waiting" PullingFsPhase = "Pulling fs layer" DownloadingPhase = "Downloading" DownloadCompletePhase = "Download complete" @@ -393,7 +392,7 @@ const ( 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 { return } @@ -403,7 +402,7 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progr total int64 percent int current int64 - status = progress.Working + status = api.Working ) text = jm.Progress.String() @@ -420,22 +419,22 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progr } } case DownloadCompletePhase, AlreadyExistsPhase, PullCompletePhase: - status = progress.Done + status = api.Done percent = 100 } if strings.Contains(jm.Status, "Image is up to date") || strings.Contains(jm.Status, "Downloaded newer image") { - status = progress.Done + status = api.Done percent = 100 } if jm.Error != nil { - status = progress.Error + status = api.Error text = jm.Error.Message } - events.On(progress.Event{ + events.On(api.Resource{ ID: jm.ID, ParentID: parent, Current: current, diff --git a/pkg/compose/push.go b/pkg/compose/push.go index 75e681e34..04b413fd9 100644 --- a/pkg/compose/push.go +++ b/pkg/compose/push.go @@ -33,14 +33,13 @@ import ( "github.com/docker/compose/v2/internal/registry" "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 { if options.Quiet { 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) }, "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 { 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, - Status: progress.Done, + Status: api.Done, Text: "Skipped", }) continue @@ -68,16 +67,16 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio for _, tag := range tags { 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) if err != nil { if !options.IgnoreFailures { - s.events.On(progress.NewEvent(tag, progress.Error, err.Error())) + s.events.On(newEvent(tag, api.Error, err.Error())) return err } - s.events.On(progress.NewEvent(tag, progress.Warning, err.Error())) + s.events.On(newEvent(tag, api.Warning, err.Error())) } else { - s.events.On(progress.NewEvent(tag, progress.Done, "Pushed")) + s.events.On(newEvent(tag, api.Done, "Pushed")) } return nil }) @@ -129,24 +128,24 @@ func (s *composeService) pushServiceImage(ctx context.Context, tag string, quiet 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 == "" { // skipped return } var ( text string - status = progress.Working + status = api.Working total int64 current int64 percent int ) if isDone(jm) { - status = progress.Done + status = api.Done percent = 100 } if jm.Error != nil { - status = progress.Error + status = api.Error text = jm.Error.Message } 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, ID: jm.ID, Text: text, diff --git a/pkg/compose/remove.go b/pkg/compose/remove.go index a41645c11..e47ad058a 100644 --- a/pkg/compose/remove.go +++ b/pkg/compose/remove.go @@ -24,8 +24,6 @@ import ( "github.com/docker/compose/v2/pkg/api" "github.com/docker/docker/api/types/container" "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 { @@ -76,8 +74,7 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options }) if len(names) == 0 { - _, _ = fmt.Fprintln(s.stdinfo(), "No stopped containers") - return nil + return api.ErrNoResources } 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 progress.Run(ctx, func(ctx context.Context) error { + return Run(ctx, func(ctx context.Context) error { return s.remove(ctx, stoppedContainers, options) }, "remove", s.events) } @@ -102,13 +99,13 @@ func (s *composeService) remove(ctx context.Context, containers Containers, opti for _, ctr := range containers { eg.Go(func() error { eventName := getContainerProgressName(ctr) - s.events.On(progress.RemovingEvent(eventName)) + s.events.On(removingEvent(eventName)) err := s.apiClient().ContainerRemove(ctx, ctr.ID, container.RemoveOptions{ RemoveVolumes: options.Volumes, Force: options.Force, }) if err == nil { - s.events.On(progress.RemovedEvent(eventName)) + s.events.On(removedEvent(eventName)) } return err }) diff --git a/pkg/compose/restart.go b/pkg/compose/restart.go index 43f54d858..905cfe44b 100644 --- a/pkg/compose/restart.go +++ b/pkg/compose/restart.go @@ -22,14 +22,13 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/utils" "github.com/docker/docker/api/types/container" "golang.org/x/sync/errgroup" ) 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) }, "restart", s.events) } @@ -93,13 +92,13 @@ func (s *composeService) restart(ctx context.Context, projectName string, option } } eventName := getContainerProgressName(ctr) - s.events.On(progress.RestartingEvent(eventName)) + s.events.On(restartingEvent(eventName)) timeout := utils.DurationSecondToInt(options.Timeout) err = s.apiClient().ContainerRestart(ctx, ctr.ID, container.StopOptions{Timeout: timeout}) if err != nil { return err } - s.events.On(progress.StartedEvent(eventName)) + s.events.On(startedEvent(eventName)) for _, hook := range def.PostStart { err = s.runHook(ctx, ctr, def, hook, nil) if err != nil { diff --git a/pkg/compose/run.go b/pkg/compose/run.go index 0f7027045..2e0ba9658 100644 --- a/pkg/compose/run.go +++ b/pkg/compose/run.go @@ -28,7 +28,6 @@ import ( "github.com/docker/cli/cli" cmd "github.com/docker/cli/cli/command/container" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/pkg/stringid" ) @@ -65,7 +64,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project, 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) }, "run", s.events) if err != nil { diff --git a/pkg/compose/scale.go b/pkg/compose/scale.go index 124c63e95..8b7acd3e3 100644 --- a/pkg/compose/scale.go +++ b/pkg/compose/scale.go @@ -21,11 +21,10 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/compose/v2/internal/tracing" "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 { - 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}) if err != nil { return err diff --git a/pkg/compose/start.go b/pkg/compose/start.go index ad691191d..ae9e0d557 100644 --- a/pkg/compose/start.go +++ b/pkg/compose/start.go @@ -23,7 +23,6 @@ import ( "strings" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" containerType "github.com/docker/docker/api/types/container" "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 { - 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) }, "start", s.events) } diff --git a/pkg/compose/stop.go b/pkg/compose/stop.go index de21ad690..730073a71 100644 --- a/pkg/compose/stop.go +++ b/pkg/compose/stop.go @@ -22,11 +22,10 @@ import ( "strings" "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 { - 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) }, "stop", s.events) } diff --git a/pkg/compose/up.go b/pkg/compose/up.go index 40d866d8a..132036741 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -33,14 +33,13 @@ import ( "github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" "github.com/eiannone/keyboard" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" ) 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) if err != nil { return err @@ -126,7 +125,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options first := true gracefulTeardown := func() { 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 { err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{ Services: options.Create.Services, @@ -162,7 +161,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options All: true, }) // 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 } @@ -205,7 +204,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options } once = false 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 { err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{ Services: options.Create.Services, diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 644a4dced..4b3fe0c13 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -33,9 +33,9 @@ import ( "github.com/docker/compose/v2/internal/sync" "github.com/docker/compose/v2/internal/tracing" "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/progress" cutils "github.com/docker/compose/v2/pkg/utils" "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/utils" @@ -472,7 +472,7 @@ func (t tarDockerClient) Exec(ctx context.Context, containerID string, cmd []str }) } eg.Go(func() error { - _, err := io.Copy(t.s.stdinfo(), conn.Reader) + _, err := io.Copy(t.s.stdout(), conn.Reader) 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)) // restrict the build to ONLY this service, not any of its dependencies options.Build.Services = services - options.Build.Progress = progress.ModePlain + options.Build.Progress = string(progressui.PlainMode) options.Build.Out = cutils.GetWriter(func(line string) { options.LogTo.Log(api.WatchLogger, line) }) diff --git a/pkg/progress/event.go b/pkg/progress/event.go deleted file mode 100644 index 5782b9345..000000000 --- a/pkg/progress/event.go +++ /dev/null @@ -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) -}