move progress UI components into cmd

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

View File

@ -26,8 +26,8 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/*
Copyright 2020 Docker Compose CLI authors
Copyright 2024 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,103 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"context"
)
// EventStatus indicates the status of an action
type EventStatus int
const (
// Working means that the current task is working
Working EventStatus = iota
// Done means that the current task is done
Done
// Warning means that the current task has warning
Warning
// Error means that the current task has errored
Error
)
// ResourceCompose is a special resource ID used when event applies to all resources in the application
const ResourceCompose = "Compose"
const (
StatusError = "Error"
StatusCreating = "Creating"
StatusStarting = "Starting"
StatusStarted = "Started"
StatusWaiting = "Waiting"
StatusHealthy = "Healthy"
StatusExited = "Exited"
StatusRestarting = "Restarting"
StatusRestarted = "Restarted"
StatusRunning = "Running"
StatusCreated = "Created"
StatusStopping = "Stopping"
StatusStopped = "Stopped"
StatusKilling = "Killing"
StatusKilled = "Killed"
StatusRemoving = "Removing"
StatusRemoved = "Removed"
StatusBuilding = "Building"
StatusBuilt = "Built"
StatusPulling = "Pulling"
StatusPulled = "Pulled"
StatusCommitting = "Committing"
StatusCommitted = "Committed"
StatusCopying = "Copying"
StatusCopied = "Copied"
StatusExporting = "Exporting"
StatusExported = "Exported"
)
// Resource represents status change and progress for a compose resource.
type Resource struct {
ID string
ParentID string
Text string
Details string
Status EventStatus
Current int64
Percent int
Total int64
}
func (e *Resource) StatusText() string {
switch e.Status {
case Working:
return "Working"
case Warning:
return "Warning"
case Done:
return "Done"
default:
return "Error"
}
}
// EventProcessor is notified about Compose operations and tasks
type EventProcessor interface {
// Start is triggered as a Compose operation is starting with context
Start(ctx context.Context, operation string)
// On notify about (sub)task and progress processing operation
On(events ...Resource)
// Done is triggered as a Compose operation completed
Done(operation string, success bool)
}

View File

@ -26,7 +26,6 @@ import (
"github.com/containerd/platforms"
"github.com/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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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=<n>] MODEL [-- <runtime-flags...>]

View File

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

View File

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

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

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

View File

@ -35,7 +35,6 @@ import (
"github.com/docker/compose/v2/internal/oci"
"github.com/docker/compose/v2/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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,234 +0,0 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package progress
import (
"context"
"fmt"
)
// EventStatus indicates the status of an action
type EventStatus int
const (
// Working means that the current task is working
Working EventStatus = iota
// Done means that the current task is done
Done
// Warning means that the current task has warning
Warning
// Error means that the current task has errored
Error
)
const (
StatusError = "Error"
StatusCreating = "Creating"
StatusStarting = "Starting"
StatusStarted = "Started"
StatusWaiting = "Waiting"
StatusHealthy = "Healthy"
StatusExited = "Exited"
StatusRestarting = "Restarting"
StatusRestarted = "Restarted"
StatusRunning = "Running"
StatusCreated = "Created"
StatusStopping = "Stopping"
StatusStopped = "Stopped"
StatusKilling = "Killing"
StatusKilled = "Killed"
StatusRemoving = "Removing"
StatusRemoved = "Removed"
StatusBuilding = "Building"
StatusBuilt = "Built"
StatusPulling = "Pulling"
StatusPulled = "Pulled"
StatusCommitting = "Committing"
StatusCommitted = "Committed"
StatusCopying = "Copying"
StatusCopied = "Copied"
StatusExporting = "Exporting"
StatusExported = "Exported"
)
// Event represents a progress event.
type Event struct {
ID string
ParentID string
Text string
Details string
Status EventStatus
Current int64
Percent int
Total int64
}
func (e *Event) StatusText() string {
switch e.Status {
case Working:
return "Working"
case Warning:
return "Warning"
case Done:
return "Done"
default:
return "Error"
}
}
// ErrorEvent creates a new Error Event with message
func ErrorEvent(id string, msg string) Event {
return Event{
ID: id,
Status: Error,
Text: StatusError,
Details: msg,
}
}
// ErrorEventf creates a new Error Event with format message
func ErrorEventf(id string, msg string, args ...any) Event {
return ErrorEvent(id, fmt.Sprintf(msg, args...))
}
// CreatingEvent creates a new Create in progress Event
func CreatingEvent(id string) Event {
return NewEvent(id, Working, StatusCreating)
}
// StartingEvent creates a new Starting in progress Event
func StartingEvent(id string) Event {
return NewEvent(id, Working, StatusStarting)
}
// StartedEvent creates a new Started in progress Event
func StartedEvent(id string) Event {
return NewEvent(id, Done, StatusStarted)
}
// Waiting creates a new waiting event
func Waiting(id string) Event {
return NewEvent(id, Working, StatusWaiting)
}
// Healthy creates a new healthy event
func Healthy(id string) Event {
return NewEvent(id, Done, StatusHealthy)
}
// Exited creates a new exited event
func Exited(id string) Event {
return NewEvent(id, Done, StatusExited)
}
// RestartingEvent creates a new Restarting in progress Event
func RestartingEvent(id string) Event {
return NewEvent(id, Working, StatusRestarting)
}
// RestartedEvent creates a new Restarted in progress Event
func RestartedEvent(id string) Event {
return NewEvent(id, Done, StatusRestarted)
}
// RunningEvent creates a new Running in progress Event
func RunningEvent(id string) Event {
return NewEvent(id, Done, StatusRunning)
}
// CreatedEvent creates a new Created (done) Event
func CreatedEvent(id string) Event {
return NewEvent(id, Done, StatusCreated)
}
// StoppingEvent creates a new Stopping in progress Event
func StoppingEvent(id string) Event {
return NewEvent(id, Working, StatusStopping)
}
// StoppedEvent creates a new Stopping in progress Event
func StoppedEvent(id string) Event {
return NewEvent(id, Done, StatusStopped)
}
// KillingEvent creates a new Killing in progress Event
func KillingEvent(id string) Event {
return NewEvent(id, Working, StatusKilling)
}
// KilledEvent creates a new Killed in progress Event
func KilledEvent(id string) Event {
return NewEvent(id, Done, StatusKilled)
}
// RemovingEvent creates a new Removing in progress Event
func RemovingEvent(id string) Event {
return NewEvent(id, Working, StatusRemoving)
}
// RemovedEvent creates a new removed (done) Event
func RemovedEvent(id string) Event {
return NewEvent(id, Done, StatusRemoved)
}
// BuildingEvent creates a new Building in progress Event
func BuildingEvent(id string) Event {
return NewEvent("Image "+id, Working, StatusBuilding)
}
// BuiltEvent creates a new built (done) Event
func BuiltEvent(id string) Event {
return NewEvent("Image "+id, Done, StatusBuilt)
}
// PullingEvent creates a new pulling (in progress) Event
func PullingEvent(id string) Event {
return NewEvent("Image "+id, Working, StatusPulling)
}
// PulledEvent creates a new pulled (done) Event
func PulledEvent(id string) Event {
return NewEvent("Image "+id, Done, StatusPulled)
}
// SkippedEvent creates a new Skipped Event
func SkippedEvent(id string, reason string) Event {
return Event{
ID: id,
Status: Warning,
Text: "Skipped: " + reason,
}
}
// NewEvent new event
func NewEvent(id string, status EventStatus, text string) Event {
return Event{
ID: id,
Status: status,
Text: text,
}
}
// EventProcessor is notified about Compose operations and tasks
type EventProcessor interface {
// Start is triggered as a Compose operation is starting with context
Start(ctx context.Context, operation string)
// On notify about (sub)task and progress processing operation
On(events ...Event)
// Done is triggered as a Compose operation completed
Done(operation string, success bool)
}