Repository: docker/compose
Pull request #11357 (2024-01-16 14:16:43)

Stop the resource timer after last expected event
**What I did**
`Done` event status doesn't mean the resource reached the expected status, it typically is first `Created` then `Started` and eventually `Healthy`. We need the TUI to be aware more events are expected and not stop the timer.

**Related issue**
fixes https://github.com/docker/compose/issues/11326

**(not mandatory) A picture of a cute animal, if possible in relation to what you did**
![631631](https://github.com/docker/compose/assets/132757/878436db-b6fb-4f85-9b34-8d94f72c95da)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker) Report
Attention: `7 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`2bf2b22`)](https://app.codecov.io/gh/docker/compose/commit/2bf2b22fbe3b62c772adf05e9b9fbb848447d9cc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker) 56.74% compared to head [(`a4ddbcb`)](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker) 56.56%.
> Report is 14 commits behind head on main.

| [Files](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker) | Patch % | Lines |
|---|---|---|
| [pkg/progress/tty.go](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker#diff-cGtnL3Byb2dyZXNzL3R0eS5nbw==) | 0.00% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker) |
| [pkg/progress/noop.go](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker#diff-cGtnL3Byb2dyZXNzL25vb3AuZ28=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker) |
| [pkg/progress/quiet.go](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker#diff-cGtnL3Byb2dyZXNzL3F1aWV0Lmdv) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main   #11357      +/-   ##
==========================================
- Coverage   56.74%   56.56%   -0.18%     
==========================================
  Files         134      136       +2     
  Lines       11448    11541      +93     
==========================================
+ Hits         6496     6528      +32     
- Misses       4330     4387      +57     
- Partials      622      626       +4     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/docker/compose/pull/11357?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=docker).


Patch:
diff --git a/go.mod b/go.mod
index 6e7693677..34547ae4c 100644
--- a/go.mod
+++ b/go.mod
@@ -8,13 +8,13 @@ require (
 	github.com/buger/goterm v1.0.4
 	github.com/compose-spec/compose-go/v2 v2.0.0-beta.3
 	github.com/containerd/console v1.0.3
-	github.com/containerd/containerd v1.7.11
+	github.com/containerd/containerd v1.7.12
 	github.com/davecgh/go-spew v1.1.1
 	github.com/distribution/reference v0.5.0
 	github.com/docker/buildx v0.12.0-rc2.0.20231219140829-617f538cb315
-	github.com/docker/cli v25.0.0-rc.1+incompatible
+	github.com/docker/cli v25.0.0-rc.2+incompatible
 	github.com/docker/cli-docs-tool v0.6.0
-	github.com/docker/docker v25.0.0-rc.1+incompatible
+	github.com/docker/docker v25.0.0-rc.2+incompatible
 	github.com/docker/go-connections v0.5.0
 	github.com/docker/go-units v0.5.0
 	github.com/fsnotify/fsevents v0.1.1
diff --git a/go.sum b/go.sum
index 8d4124928..d89096e7d 100644
--- a/go.sum
+++ b/go.sum
@@ -92,8 +92,8 @@ github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaD
 github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
 github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
-github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw=
-github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE=
+github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0=
+github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk=
 github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
 github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
 github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
@@ -122,15 +122,15 @@ github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK
 github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 github.com/docker/buildx v0.12.0-rc2.0.20231219140829-617f538cb315 h1:UZxx9xBADdf/9UmSdEUi+pdJoPKpgcf9QUAY5gEIYmY=
 github.com/docker/buildx v0.12.0-rc2.0.20231219140829-617f538cb315/go.mod h1:X8ZHhuW6ncwtoJ36TlU+gyaROTcBkTE01VHYmTStQCE=
-github.com/docker/cli v25.0.0-rc.1+incompatible h1:TRaJCO3nQ0XvMWa8gUeIKRO8Dq61QQClpMWV0PmSDHA=
-github.com/docker/cli v25.0.0-rc.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v25.0.0-rc.2+incompatible h1:3k4Qxnzde5uEE1gutTovj1TT9vTvzna/2dEERHZX0wM=
+github.com/docker/cli v25.0.0-rc.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/cli-docs-tool v0.6.0 h1:Z9x10SaZgFaB6jHgz3OWooynhSa40CsWkpe5hEnG/qA=
 github.com/docker/cli-docs-tool v0.6.0/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v25.0.0-rc.1+incompatible h1:Bdb06U1Z1P78uxluMZE6MI94tGICXzWnArsiW5hg6pU=
-github.com/docker/docker v25.0.0-rc.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v25.0.0-rc.2+incompatible h1:vXdPCzJf8tX/cO3yBjRj1s2NyapJ3jOlcwGQZaLxidk=
+github.com/docker/docker v25.0.0-rc.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
 github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
 github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@@ -382,8 +382,6 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
 github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
 github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
-github.com/opencontainers/runc v1.1.10 h1:EaL5WeO9lv9wmS6SASjszOeQdSctvpbu0DdBQBizE40=
-github.com/opencontainers/runc v1.1.10/go.mod h1:+/R6+KmDlh+hOO8NkjmgkG9Qzvypzk0yXxAPYYR65+M=
 github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg=
 github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go
index 4ba31ff09..1f9dac1c0 100644
--- a/pkg/compose/convergence.go
+++ b/pkg/compose/convergence.go
@@ -737,7 +737,7 @@ func (s *composeService) isServiceCompleted(ctx context.Context, containers Cont
 	return false, 0, nil
 }
 
-func (s *composeService) startService(ctx context.Context, project *types.Project, service types.ServiceConfig, containers Containers) error {
+func (s *composeService) startService(ctx context.Context, project *types.Project, service types.ServiceConfig, containers Containers, wait bool) error {
 	if service.Deploy != nil && service.Deploy.Replicas != nil && *service.Deploy.Replicas == 0 {
 		return nil
 	}
@@ -765,11 +765,28 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
 		if err != nil {
 			return err
 		}
-		w.Event(progress.StartedEvent(eventName))
+		status := progress.Done
+		if wait || dependencyWaiting(project, service.Name) {
+			status = progress.Working
+		}
+		w.Event(progress.NewEvent(eventName, status, "Started"))
 	}
 	return nil
 }
 
+func dependencyWaiting(project *types.Project, name string) bool {
+	for _, service := range project.Services {
+		depends, ok := service.DependsOn[name]
+		if !ok {
+			continue
+		}
+		if depends.Condition == types.ServiceConditionHealthy {
+			return true
+		}
+	}
+	return false
+}
+
 func mergeLabels(ls ...types.Labels) types.Labels {
 	merged := types.Labels{}
 	for _, l := range ls {
diff --git a/pkg/compose/start.go b/pkg/compose/start.go
index dd35a2e94..c87788b94 100644
--- a/pkg/compose/start.go
+++ b/pkg/compose/start.go
@@ -129,7 +129,7 @@ func (s *composeService) start(ctx context.Context, projectName string, options
 			return err
 		}
 
-		return s.startService(ctx, project, service, containers)
+		return s.startService(ctx, project, service, containers, options.Wait)
 	})
 	if err != nil {
 		return err
diff --git a/pkg/compose/up.go b/pkg/compose/up.go
index 8236adff0..be6bbdb14 100644
--- a/pkg/compose/up.go
+++ b/pkg/compose/up.go
@@ -33,11 +33,14 @@ import (
 
 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(project), func(ctx context.Context) error {
+		w := progress.ContextWriter(ctx)
+		w.HasMore(options.Start.Attach == nil)
 		err := s.create(ctx, project, options.Create)
 		if err != nil {
 			return err
 		}
 		if options.Start.Attach == nil {
+			w.HasMore(false)
 			return s.start(ctx, project.Name, options.Start, nil)
 		}
 		return nil
diff --git a/pkg/progress/event.go b/pkg/progress/event.go
index 26aca98d8..32dbac6af 100644
--- a/pkg/progress/event.go
+++ b/pkg/progress/event.go
@@ -191,6 +191,6 @@ func (e *Event) Spinner() any {
 	case Error:
 		return ErrorColor(spinnerError)
 	default:
-		return e.spinner.String()
+		return CountColor(e.spinner.String())
 	}
 }
diff --git a/pkg/progress/noop.go b/pkg/progress/noop.go
index 98852705c..57a9d9770 100644
--- a/pkg/progress/noop.go
+++ b/pkg/progress/noop.go
@@ -38,3 +38,6 @@ func (p *noopWriter) TailMsgf(_ string, _ ...interface{}) {
 
 func (p *noopWriter) Stop() {
 }
+
+func (p *noopWriter) HasMore(bool) {
+}
diff --git a/pkg/progress/plain.go b/pkg/progress/plain.go
index 4a061e183..bd66984a4 100644
--- a/pkg/progress/plain.go
+++ b/pkg/progress/plain.go
@@ -64,3 +64,6 @@ func (p *plainWriter) TailMsgf(msg string, args ...interface{}) {
 func (p *plainWriter) Stop() {
 	p.done <- true
 }
+
+func (p *plainWriter) HasMore(bool) {
+}
diff --git a/pkg/progress/quiet.go b/pkg/progress/quiet.go
index 5c5530d76..b9f2765eb 100644
--- a/pkg/progress/quiet.go
+++ b/pkg/progress/quiet.go
@@ -35,3 +35,6 @@ func (q quiet) Events(_ []Event) {
 
 func (q quiet) TailMsgf(_ string, _ ...interface{}) {
 }
+
+func (q quiet) HasMore(bool) {
+}
diff --git a/pkg/progress/tty.go b/pkg/progress/tty.go
index c6b806819..20322636d 100644
--- a/pkg/progress/tty.go
+++ b/pkg/progress/tty.go
@@ -33,17 +33,18 @@ import (
 )
 
 type ttyWriter struct {
-	out             io.Writer
-	events          map[string]Event
-	eventIDs        []string
-	repeated        bool
-	numLines        int
-	done            chan bool
-	mtx             *sync.Mutex
-	tailEvents      []string
-	dryRun          bool
-	skipChildEvents bool
-	progressTitle   string
+	out               io.Writer
+	events            map[string]Event
+	eventIDs          []string
+	repeated          bool
+	numLines          int
+	done              chan bool
+	mtx               *sync.Mutex
+	tailEvents        []string
+	dryRun            bool
+	skipChildEvents   bool
+	progressTitle     string
+	hasFollowupAction bool
 }
 
 func (w *ttyWriter) Start(ctx context.Context) error {
@@ -70,6 +71,10 @@ func (w *ttyWriter) Stop() {
 	w.done <- true
 }
 
+func (w *ttyWriter) HasMore(b bool) {
+	w.hasFollowupAction = b
+}
+
 func (w *ttyWriter) Event(e Event) {
 	w.mtx.Lock()
 	defer w.mtx.Unlock()
@@ -82,6 +87,9 @@ func (w *ttyWriter) event(e Event) {
 	}
 	if _, ok := w.events[e.ID]; ok {
 		last := w.events[e.ID]
+		if e.Status == Done && w.hasFollowupAction {
+			e.Status = Working
+		}
 		switch e.Status {
 		case Done, Error, Warning:
 			if last.endTime.IsZero() {
diff --git a/pkg/progress/writer.go b/pkg/progress/writer.go
index f1dec3c79..b6d60e3da 100644
--- a/pkg/progress/writer.go
+++ b/pkg/progress/writer.go
@@ -36,6 +36,7 @@ type Writer interface {
 	Event(Event)
 	Events([]Event)
 	TailMsgf(string, ...interface{})
+	HasMore(more bool)
 }
 
 type writerKey struct{}


Test patch:
diff --git a/pkg/progress/tty_test.go b/pkg/progress/tty_test.go
index 6ffdf2bec..fc484b7c0 100644
--- a/pkg/progress/tty_test.go
+++ b/pkg/progress/tty_test.go
@@ -42,7 +42,7 @@ func TestLineText(t *testing.T) {
 	lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text))
 
 	out := tty().lineText(ev, "", 50, lineWidth, false)
-	assert.Equal(t, out, " . id Text Status                            \x1b[34m0.0s \x1b[0m\n")
+	assert.Equal(t, out, " \x1b[33m.\x1b[0m id Text Status                            \x1b[34m0.0s \x1b[0m\n")
 
 	ev.Status = Done
 	out = tty().lineText(ev, "", 50, lineWidth, false)