distinguish event (short) status text and details

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof
2025-11-03 10:47:02 +01:00
committed by Guillaume Lours
parent bff3d35305
commit d70bb8cf5e
27 changed files with 255 additions and 244 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ bin/
coverage.out
covdatafiles/
.DS_Store
pkg/e2e/*.tar

1
go.mod
View File

@@ -63,6 +63,7 @@ require (
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect

3
go.sum
View File

@@ -4,8 +4,9 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=

View File

@@ -41,21 +41,18 @@ func (s *composeService) commit(ctx context.Context, projectName string, options
}
name := getCanonicalContainerName(ctr)
msg := fmt.Sprintf("Commit %s", name)
s.events.On(progress.Event{
ID: name,
Text: msg,
Status: progress.Working,
StatusText: "Committing",
ID: name,
Status: progress.Working,
Text: progress.StatusCommitting,
})
if s.dryRun {
s.events.On(progress.Event{
ID: name,
Text: msg,
Status: progress.Done,
StatusText: "Committed",
ID: name,
Status: progress.Done,
Text: progress.StatusCommitted,
})
return nil
@@ -73,10 +70,9 @@ func (s *composeService) commit(ctx context.Context, projectName string, options
}
s.events.On(progress.Event{
ID: name,
Text: msg,
Status: progress.Done,
StatusText: fmt.Sprintf("Committed as %s", response.ID),
ID: name,
Text: fmt.Sprintf("Committed as %s", response.ID),
Status: progress.Done,
})
return nil

View File

@@ -504,7 +504,9 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
logrus.Warnf("optional dependency %q failed to start: %s", dep, err.Error())
return nil
}
s.events.On(containerEvents(waitingFor, progress.ErrorEvent)...)
s.events.On(containerEvents(waitingFor, func(s string) progress.Event {
return progress.ErrorEventf(s, "dependency %s failed to start", dep)
})...)
return fmt.Errorf("dependency failed to start: %w", err)
}
if healthy {
@@ -532,7 +534,9 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
}
msg := fmt.Sprintf("service %s", messageSuffix)
s.events.On(containerReasonEvents(waitingFor, progress.ErrorMessageEvent, msg)...)
s.events.On(containerEvents(waitingFor, func(s string) progress.Event {
return progress.ErrorEventf(s, "service %s", messageSuffix)
})...)
return errors.New(msg)
}
default:
@@ -600,9 +604,9 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro
if err != nil {
if ctx.Err() == nil {
s.events.On(progress.Event{
ID: eventName,
Status: progress.Error,
StatusText: err.Error(),
ID: eventName,
Status: progress.Error,
Text: err.Error(),
})
}
return ctr, err
@@ -619,9 +623,9 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
defer func() {
if err != nil && ctx.Err() == nil {
s.events.On(progress.Event{
ID: eventName,
Status: progress.Error,
StatusText: err.Error(),
ID: eventName,
Status: progress.Error,
Text: err.Error(),
})
}
}()

View File

@@ -86,24 +86,24 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
name := getCanonicalContainerName(ctr)
var msg string
if direction == fromService {
msg = fmt.Sprintf("copy %s:%s to %s", name, srcPath, dstPath)
msg = fmt.Sprintf("%s:%s to %s", name, srcPath, dstPath)
} else {
msg = fmt.Sprintf("copy %s to %s:%s", srcPath, name, dstPath)
msg = fmt.Sprintf("%s to %s:%s", srcPath, name, dstPath)
}
s.events.On(progress.Event{
ID: name,
Text: msg,
Status: progress.Working,
StatusText: "Copying",
ID: name,
Text: progress.StatusCopying,
Details: msg,
Status: progress.Working,
})
if err := copyFunc(ctx, ctr.ID, srcPath, dstPath, options); err != nil {
return err
}
s.events.On(progress.Event{
ID: name,
Text: msg,
Status: progress.Done,
StatusText: "Copied",
ID: name,
Text: progress.StatusCopied,
Details: msg,
Status: progress.Done,
})
return nil
})

View File

@@ -1398,7 +1398,7 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
resp, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts)
if err != nil {
s.events.On(progress.ErrorEvent(networkEventName))
s.events.On(progress.ErrorEvent(networkEventName, err.Error()))
return "", fmt.Errorf("failed to create network %s: %w", n.Name, err)
}
s.events.On(progress.CreatedEvent(networkEventName))
@@ -1632,7 +1632,7 @@ func (s *composeService) createVolume(ctx context.Context, volume types.VolumeCo
DriverOpts: volume.DriverOpts,
})
if err != nil {
s.events.On(progress.ErrorEvent(eventName))
s.events.On(progress.ErrorEvent(eventName, err.Error()))
return err
}
s.events.On(progress.CreatedEvent(eventName))

View File

@@ -235,7 +235,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
if errdefs.IsNotFound(err) {
continue
}
s.events.On(progress.ErrorEvent(eventName))
s.events.On(progress.ErrorEvent(eventName, err.Error()))
return fmt.Errorf("failed to remove network %s: %w", name, err)
}
s.events.On(progress.RemovedEvent(eventName))
@@ -317,7 +317,7 @@ 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.ErrorMessageEvent(eventName, "Error while Stopping"))
s.events.On(progress.ErrorEvent(eventName, "Error while Stopping"))
return err
}
s.events.On(progress.StoppedEvent(eventName))
@@ -360,7 +360,7 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
RemoveVolumes: volumes,
})
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
s.events.On(progress.ErrorMessageEvent(eventName, "Error while Removing"))
s.events.On(progress.ErrorEvent(eventName, "Error while Removing"))
return err
}
s.events.On(progress.RemovedEvent(eventName))

View File

@@ -51,13 +51,10 @@ func (s *composeService) export(ctx context.Context, projectName string, options
}
name := getCanonicalContainerName(container)
msg := fmt.Sprintf("export %s to %s", name, options.Output)
s.events.On(progress.Event{
ID: name,
Text: msg,
Status: progress.Working,
StatusText: "Exporting",
ID: name,
Text: progress.StatusExporting,
Status: progress.Working,
})
responseBody, err := s.apiClient().ContainerExport(ctx, container.ID)
@@ -67,12 +64,7 @@ func (s *composeService) export(ctx context.Context, projectName string, options
defer func() {
if err := responseBody.Close(); err != nil {
s.events.On(progress.Event{
ID: name,
Text: msg,
Status: progress.Error,
StatusText: fmt.Sprintf("Failed to close response body: %v", err),
})
s.events.On(progress.ErrorEventf(name, "Failed to close response body: %s", err.Error()))
}
}()
@@ -93,10 +85,9 @@ func (s *composeService) export(ctx context.Context, projectName string, options
}
s.events.On(progress.Event{
ID: name,
Text: msg,
Status: progress.Done,
StatusText: "Exported",
ID: name,
Text: progress.StatusExported,
Status: progress.Done,
})
return nil

View File

@@ -66,7 +66,7 @@ func (s *composeService) kill(ctx context.Context, projectName string, options a
s.events.On(progress.KillingEvent(eventName))
err := s.apiClient().ContainerKill(ctx, ctr.ID, options.Signal)
if err != nil {
s.events.On(progress.ErrorMessageEvent(eventName, "Error while Killing"))
s.events.On(progress.ErrorEvent(eventName, "Error while Killing"))
return err
}
s.events.On(progress.KilledEvent(eventName))

View File

@@ -132,31 +132,30 @@ func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quiet
if !quietPull {
events.On(progress.Event{
ID: model.Name,
Status: progress.Working,
Text: "Pulling",
StatusText: msg,
ID: model.Name,
Status: progress.Working,
Text: progress.StatusPulling,
})
}
}
err = cmd.Wait()
if err != nil {
events.On(progress.ErrorMessageEvent(model.Name, err.Error()))
events.On(progress.ErrorEvent(model.Name, err.Error()))
}
events.On(progress.Event{
ID: model.Name,
Status: progress.Working,
StatusText: "Pulled",
ID: model.Name,
Status: progress.Working,
Text: progress.StatusPulled,
})
return err
}
func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events progress.EventProcessor) error {
events.On(progress.Event{
ID: config.Name,
Status: progress.Working,
StatusText: "Configuring",
ID: config.Name,
Status: progress.Working,
Text: "Configuring",
})
// configure [--context-size=<n>] MODEL [-- <runtime-flags...>]
args := []string{"configure"}

View File

@@ -143,7 +143,7 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
err = cmd.Wait()
if err != nil {
s.events.On(progress.ErrorMessageEvent(service.Name, err.Error()))
s.events.On(progress.ErrorEvent(service.Name, err.Error()))
return nil, fmt.Errorf("failed to %s service provider: %s", action, err.Error())
}
switch command {

View File

@@ -68,9 +68,10 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
for name, service := range project.Services {
if service.Image == "" {
s.events.On(progress.Event{
ID: name,
Status: progress.Done,
Text: "Skipped - No image to be pulled",
ID: name,
Status: progress.Done,
Text: "Skipped",
Details: "No image to be pulled",
})
continue
}
@@ -86,9 +87,10 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
case types.PullPolicyMissing, types.PullPolicyIfNotPresent:
if imageAlreadyPresent(service.Image, images) {
s.events.On(progress.Event{
ID: "Image " + service.Image,
Status: progress.Done,
Text: "Skipped - Image is already present locally",
ID: "Image " + service.Image,
Status: progress.Done,
Text: "Skipped",
Details: "Image is already present locally",
})
continue
}
@@ -96,9 +98,10 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
if service.Build != nil && opts.IgnoreBuildable {
s.events.On(progress.Event{
ID: "Image " + service.Image,
Status: progress.Done,
Text: "Skipped - Image can be built",
ID: "Image " + service.Image,
Status: progress.Done,
Text: "Skipped",
Details: "Image can be built",
})
continue
}
@@ -119,11 +122,8 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
}
if !opts.IgnoreFailures && service.Build == nil {
if s.dryRun {
s.events.On(progress.Event{
ID: "Image " + service.Image,
Status: progress.Error,
Text: fmt.Sprintf(" - Pull error for image: %s", service.Image),
})
s.events.On(progress.ErrorEventf("Image "+service.Image,
"error pulling image: %s", service.Image))
}
// fail fast if image can't be pulled nor built
return err
@@ -197,9 +197,9 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
if ctx.Err() != nil {
s.events.On(progress.Event{
ID: resource,
Status: progress.Warning,
StatusText: "Interrupted",
ID: resource,
Status: progress.Warning,
Text: "Interrupted",
})
return "", nil
}
@@ -208,21 +208,15 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
// then the status should be warning instead of error
if err != nil && service.Build != nil {
s.events.On(progress.Event{
ID: resource,
Status: progress.Warning,
Text: "Warning",
StatusText: getUnwrappedErrorMessage(err),
ID: resource,
Status: progress.Warning,
Text: getUnwrappedErrorMessage(err),
})
return "", err
}
if err != nil {
s.events.On(progress.Event{
ID: resource,
Status: progress.Error,
Text: "Error",
StatusText: getUnwrappedErrorMessage(err),
})
s.events.On(progress.ErrorEvent(resource, getUnwrappedErrorMessage(err)))
return "", err
}
@@ -442,13 +436,12 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progr
}
events.On(progress.Event{
ID: jm.ID,
ParentID: parent,
Current: current,
Total: total,
Percent: percent,
Text: jm.Status,
Status: status,
StatusText: text,
ID: jm.ID,
ParentID: parent,
Current: current,
Total: total,
Percent: percent,
Status: status,
Text: text,
})
}

View File

@@ -161,14 +161,13 @@ func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, events progr
}
events.On(progress.Event{
ParentID: prefix,
ID: jm.ID,
Text: jm.Status,
Status: status,
Current: current,
Total: total,
Percent: percent,
StatusText: text,
ParentID: prefix,
ID: jm.ID,
Text: text,
Status: status,
Current: current,
Total: total,
Percent: percent,
})
}

View File

@@ -536,7 +536,7 @@ func TestBuildDependsOn(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-dependencies/compose-depends_on.yaml", "--progress=plain", "up", "test2")
out := res.Combined()
assert.Check(t, strings.Contains(out, "test1 Built"))
assert.Check(t, strings.Contains(out, "test1 Built"))
}
func TestBuildSubset(t *testing.T) {
@@ -548,7 +548,7 @@ func TestBuildSubset(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/subset/compose.yaml", "build", "main")
out := res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
assert.Check(t, strings.Contains(out, "main Built"))
}
func TestBuildDependentImage(t *testing.T) {
@@ -560,11 +560,11 @@ func TestBuildDependentImage(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "firstbuild")
out := res.Combined()
assert.Check(t, strings.Contains(out, "firstbuild Built"))
assert.Check(t, strings.Contains(out, "firstbuild Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/dependencies/compose.yaml", "build", "secondbuild")
out = res.Combined()
assert.Check(t, strings.Contains(out, "secondbuild Built"))
assert.Check(t, strings.Contains(out, "secondbuild Built"))
}
func TestBuildSubDependencies(t *testing.T) {
@@ -576,11 +576,11 @@ func TestBuildSubDependencies(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "build", "main")
out := res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
assert.Check(t, strings.Contains(out, "main Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/sub-dependencies/compose.yaml", "up", "--build", "main")
out = res.Combined()
assert.Check(t, strings.Contains(out, "main Built"))
assert.Check(t, strings.Contains(out, "main Built"))
}
func TestBuildLongOutputLine(t *testing.T) {
@@ -592,11 +592,11 @@ func TestBuildLongOutputLine(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "build", "long-line")
out := res.Combined()
assert.Check(t, strings.Contains(out, "long-line Built"))
assert.Check(t, strings.Contains(out, "long-line Built"))
res = c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/long-output-line/compose.yaml", "up", "--build", "long-line")
out = res.Combined()
assert.Check(t, strings.Contains(out, "long-line Built"))
assert.Check(t, strings.Contains(out, "long-line Built"))
}
func TestBuildDependentImageWithProfile(t *testing.T) {
@@ -608,7 +608,7 @@ func TestBuildDependentImageWithProfile(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "fixtures/build-test/profiles/compose.yaml", "build", "secret-build-test")
out := res.Combined()
assert.Check(t, strings.Contains(out, "secret-build-test Built"))
assert.Check(t, strings.Contains(out, "secret-build-test Built"))
}
func TestBuildTLS(t *testing.T) {

View File

@@ -187,8 +187,8 @@ func TestLocalComposeRun(t *testing.T) {
res.Assert(t, icmd.Success)
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/pull.yaml", "run", "--pull", "always", "backend")
assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulling"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulled"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulling"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Image nginx Pulled"), res.Combined())
})
t.Run("compose run --env-from-file", func(t *testing.T) {

View File

@@ -213,10 +213,10 @@ func TestNetworkRecreate(t *testing.T) {
err := res.Stderr()
fmt.Println(err)
res.Assert(t, icmd.Expected{Err: `
Container network_recreate-web-1 Stopped
Network network_recreate_test Removed
Network network_recreate_test Creating
Network network_recreate_test Created
Container network_recreate-web-1 Starting
Container network_recreate-web-1 Started`})
Container network_recreate-web-1 Stopped
Network network_recreate_test Removed
Network network_recreate_test Creating
Network network_recreate_test Created
Container network_recreate-web-1 Starting
Container network_recreate-web-1 Started`})
}

View File

@@ -38,7 +38,7 @@ func TestPs(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
assert.Contains(t, res.Combined(), "Container e2e-ps-busybox-1 Started", res.Combined())
assert.Contains(t, res.Combined(), "Container e2e-ps-busybox-1 Started", res.Combined())
t.Run("table", func(t *testing.T) {
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/ps-test/compose.yaml", "--project-name", projectName, "ps")

View File

@@ -34,15 +34,15 @@ func TestComposePull(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/simple", "pull")
output := res.Combined()
assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled"))
assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled"))
assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled"))
assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled"))
// verify default policy is 'always' for pull command
res = c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/simple", "pull")
output = res.Combined()
assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled"))
assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled"))
assert.Assert(t, strings.Contains(output, "Image alpine:3.14 Pulled"))
assert.Assert(t, strings.Contains(output, "Image alpine:3.15 Pulled"))
})
t.Run("Verify skipped pull if image is already present locally", func(t *testing.T) {
@@ -52,16 +52,16 @@ func TestComposePull(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/image-present-locally", "pull")
output := res.Combined()
assert.Assert(t, strings.Contains(output, "alpine:3.13.12 Skipped - Image is already present locally"))
assert.Assert(t, strings.Contains(output, "alpine:3.13.12 Skipped Image is already present locally"))
// image with :latest tag gets pulled regardless if pull_policy: missing or if_not_present
assert.Assert(t, strings.Contains(output, "alpine:latest Pulled"))
assert.Assert(t, strings.Contains(output, "alpine:latest Pulled"))
})
t.Run("Verify skipped no image to be pulled", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/compose-pull/no-image-name-given", "pull")
output := res.Combined()
assert.Assert(t, strings.Contains(output, "Skipped - No image to be pulled"))
assert.Assert(t, strings.Contains(output, "Skipped No image to be pulled"))
})
t.Run("Verify pull failure", func(t *testing.T) {

View File

@@ -42,7 +42,7 @@ func TestRestart(t *testing.T) {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose.yaml", "--project-name", projectName, "up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-restart-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-restart-1 Started"), res.Combined())
c.WaitForCmdResult(t, c.NewDockerComposeCmd(t, "--project-name", projectName, "ps", "-a", "--format",
"json"),
@@ -80,9 +80,9 @@ func TestRestartWithDependencies(t *testing.T) {
res := c.RunDockerComposeCmd(t, "restart", baseService)
out := res.Combined()
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Restarting", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Restarting", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out)
assert.Assert(t, !strings.Contains(out, depNoRestart), out)
c = NewParallelCLI(t, WithEnv(
@@ -91,11 +91,11 @@ func TestRestartWithDependencies(t *testing.T) {
))
res = c.RunDockerComposeCmd(t, "-f", "./fixtures/restart-test/compose-depends-on.yaml", "up", "-d")
out = res.Combined()
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Stopped", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Recreated", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Running", depNoRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Stopped", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Recreated", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Healthy", baseService)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Started", depWithRestart)), out)
assert.Assert(t, strings.Contains(out, fmt.Sprintf("Container e2e-restart-deps-%s-1 Running", depNoRestart)), out)
}
func TestRestartWithProfiles(t *testing.T) {
@@ -111,5 +111,5 @@ func TestRestartWithProfiles(t *testing.T) {
res := c.RunDockerComposeCmd(t, "restart", "test")
fmt.Println(res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-profiles-test-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-restart-profiles-test-1 Started"), res.Combined())
}

View File

@@ -39,7 +39,7 @@ func TestStartStop(t *testing.T) {
t.Run("Up a project", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "--project-name", projectName, "up",
"-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-no-dependencies-simple-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-no-dependencies-simple-1 Started"), res.Combined())
res = c.RunDockerComposeCmd(t, "ls", "--all")
testify.Regexp(t, getProjectRegx("running"), res.Stdout())
@@ -87,14 +87,14 @@ func TestStartStopWithDependencies(t *testing.T) {
t.Run("Up", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "--project-name", projectName,
"up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined())
})
t.Run("stop foo", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop", "foo")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Stopped"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Stopped"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running")
assert.Assert(t, strings.Contains(res.Combined(), "e2e-start-stop-with-dependencies-bar-1"), res.Combined())
@@ -103,12 +103,12 @@ func TestStartStopWithDependencies(t *testing.T) {
t.Run("start foo", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "--project-name", projectName, "stop")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Stopped"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Stopped"), res.Combined())
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "start", "foo")
out := res.Combined()
assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-bar-1 Started"), out)
assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-foo-1 Started"), out)
assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-bar-1 Started"), out)
assert.Assert(t, strings.Contains(out, "Container e2e-start-stop-with-dependencies-foo-1 Started"), out)
res = c.RunDockerComposeCmd(t, "--project-name", projectName, "ps", "--status", "running")
out = res.Combined()
@@ -120,8 +120,8 @@ func TestStartStopWithDependencies(t *testing.T) {
_ = c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/links/compose.yaml", "--project-name", projectName, "up",
"--no-deps", "-d", "foo")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-foo-1 Started"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-with-dependencies-bar-1 Started"), res.Combined())
})
t.Run("down", func(t *testing.T) {
@@ -136,8 +136,8 @@ func TestStartStopWithOneOffs(t *testing.T) {
t.Run("Up", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/dependencies/compose.yaml", "--project-name", projectName,
"up", "-d")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-foo-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-bar-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-foo-1 Started"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-with-oneoffs-bar-1 Started"), res.Combined())
})
t.Run("run one-off", func(t *testing.T) {
@@ -213,7 +213,7 @@ func TestStopAlreadyStopped(t *testing.T) {
// container is already stopped
res.Assert(t, icmd.Expected{
ExitCode: 0,
Err: "Container e2e-start-stop-svc-already-stopped-simple-1 Stopped",
Err: "Container e2e-start-stop-svc-already-stopped-simple-1 Stopped",
})
}
@@ -230,14 +230,14 @@ func TestStartStopMultipleServices(t *testing.T) {
res := cli.RunDockerComposeCmd(t, "stop", "simple", "another")
services := []string{"simple", "another"}
for _, svc := range services {
stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc)
stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc)
assert.Assert(t, strings.Contains(res.Stderr(), stopMsg),
fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined()))
}
res = cli.RunDockerComposeCmd(t, "start", "simple", "another")
for _, svc := range services {
startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc)
startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc)
assert.Assert(t, strings.Contains(res.Stderr(), startMsg),
fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined()))
}
@@ -256,7 +256,7 @@ func TestStartSingleServiceAndDependency(t *testing.T) {
res := cli.RunDockerComposeCmd(t, "start", "desired")
desiredServices := []string{"desired", "dep_1", "dep_2"}
for _, s := range desiredServices {
startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1 Started", s)
startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1 Started", s)
assert.Assert(t, strings.Contains(res.Combined(), startMsg),
fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined()))
}
@@ -277,8 +277,8 @@ func TestStartStopMultipleFiles(t *testing.T) {
cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/other.yaml", "up", "-d")
res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "stop")
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-simple-1 Stopped"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-another-1 Stopped"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-a-different-one-1 Stopped"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-and-another-one-1 Stopped"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-simple-1 Stopped"), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-another-1 Stopped"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-a-different-one-1 Stopped"), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-files-and-another-one-1 Stopped"), res.Combined())
}

View File

@@ -175,8 +175,8 @@ func TestUpWithAllResources(t *testing.T) {
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/resources/compose.yaml", "--all-resources", "--project-name", projectName, "up")
assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Volume %s_my_vol Created`, projectName)), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Network %s_my_net Created`, projectName)), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Volume %s_my_vol Created`, projectName)), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), fmt.Sprintf(`Network %s_my_net Created`, projectName)), res.Combined())
}
func TestUpProfile(t *testing.T) {
@@ -187,9 +187,9 @@ func TestUpProfile(t *testing.T) {
})
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/profiles/docker-compose.yaml", "--project-name", projectName, "up", "foo")
assert.Assert(t, strings.Contains(res.Combined(), `Container db_c Created`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `Container foo_c Created`), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), `Container bar_c Created`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `Container db_c Created`), res.Combined())
assert.Assert(t, strings.Contains(res.Combined(), `Container foo_c Created`), res.Combined())
assert.Assert(t, !strings.Contains(res.Combined(), `Container bar_c Created`), res.Combined())
}
func TestUpImageID(t *testing.T) {
@@ -218,7 +218,7 @@ func TestUpStopWithLogsMixed(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/stop/compose.yaml", "--project-name", projectName, "up", "--abort-on-container-exit")
// assert we still get service2 logs after service 1 Stopped event
res.Assert(t, icmd.Expected{
Err: "Container compose-e2e-stop-logs-service1-1 Stopped",
Err: "Container compose-e2e-stop-logs-service1-1 Stopped",
})
// assert we get stop hook logs
res.Assert(t, icmd.Expected{Out: "service2-1 -> | stop hook running...\nservice2-1 | 64 bytes"})

View File

@@ -16,7 +16,10 @@
package progress
import "context"
import (
"context"
"fmt"
)
// EventStatus indicates the status of an action
type EventStatus int
@@ -54,28 +57,52 @@ const (
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
Status EventStatus
StatusText string
Current int64
Percent int
Total int64
ID string
ParentID string
Text string
Details string
Status EventStatus
Current int64
Percent int
Total int64
}
// ErrorMessageEvent creates a new Error Event with message
func ErrorMessageEvent(id string, msg string) Event {
return NewEvent(id, Error, msg)
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
func ErrorEvent(id string) Event {
return NewEvent(id, Error, StatusError)
// 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
@@ -181,18 +208,18 @@ func PulledEvent(id string) Event {
// SkippedEvent creates a new Skipped Event
func SkippedEvent(id string, reason string) Event {
return Event{
ID: id,
Status: Warning,
StatusText: "Skipped: " + reason,
ID: id,
Status: Warning,
Text: "Skipped: " + reason,
}
}
// NewEvent new event
func NewEvent(id string, status EventStatus, statusText string) Event {
func NewEvent(id string, status EventStatus, text string) Event {
return Event{
ID: id,
Status: status,
StatusText: statusText,
ID: id,
Status: status,
Text: text,
}
}

View File

@@ -39,8 +39,9 @@ type jsonMessage struct {
Tail bool `json:"tail,omitempty"`
ID string `json:"id,omitempty"`
ParentID string `json:"parent_id,omitempty"`
Text string `json:"text,omitempty"`
Status string `json:"status,omitempty"`
Text string `json:"text,omitempty"`
Details string `json:"details,omitempty"`
Current int64 `json:"current,omitempty"`
Total int64 `json:"total,omitempty"`
Percent int `json:"percent,omitempty"`
@@ -54,8 +55,9 @@ func (p *jsonWriter) Event(e Event) {
DryRun: p.dryRun,
Tail: false,
ID: e.ID,
Status: e.StatusText(),
Text: e.Text,
Status: e.StatusText,
Details: e.Details,
ParentID: e.ParentID,
Current: e.Current,
Total: e.Total,

View File

@@ -32,13 +32,13 @@ func TestJsonWriter_Event(t *testing.T) {
}
event := Event{
ID: "service1",
ParentID: "project",
Text: "Creating",
StatusText: "Working",
Current: 50,
Total: 100,
Percent: 50,
ID: "service1",
ParentID: "project",
Status: Working,
Text: StatusCreating,
Current: 50,
Total: 100,
Percent: 50,
}
w.Event(event)
@@ -50,8 +50,8 @@ func TestJsonWriter_Event(t *testing.T) {
DryRun: true,
ID: event.ID,
ParentID: event.ParentID,
Text: event.Text,
Status: event.StatusText,
Text: StatusCreating,
Status: "Working",
Current: event.Current,
Total: event.Total,
Percent: event.Percent,

View File

@@ -43,7 +43,7 @@ func (p *plainWriter) Event(e Event) {
if p.dryRun {
prefix = api.DRYRUN_PREFIX
}
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.StatusText)
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.Details)
}
func (p *plainWriter) On(events ...Event) {

View File

@@ -58,17 +58,17 @@ type ttyWriter struct {
}
type task struct {
ID string
parentID string
startTime time.Time
endTime time.Time
text string
status EventStatus
statusText string
current int64
percent int
total int64
spinner *Spinner
ID string
parentID string
startTime time.Time
endTime time.Time
text string
details string
status EventStatus
current int64
percent int
total int64
spinner *Spinner
}
func (t *task) stop() {
@@ -112,7 +112,7 @@ func (w *ttyWriter) On(events ...Event) {
w.mtx.Lock()
defer w.mtx.Unlock()
for _, e := range events {
if w.operation != "start" && (e.StatusText == "Started" || e.StatusText == "Starting") {
if w.operation != "start" && (e.Text == StatusStarted || e.Text == StatusStarting) {
// skip those events to avoid mix with container logs
continue
}
@@ -122,8 +122,7 @@ func (w *ttyWriter) On(events ...Event) {
func (w *ttyWriter) event(e Event) {
// Suspend print while a build is in progress, to avoid collision with buildkit Display
if e.StatusText == StatusBuilding {
fmt.Println("suspend during build")
if e.Text == StatusBuilding {
w.ticker.Stop()
w.suspended = true
} else if w.suspended {
@@ -142,7 +141,7 @@ func (w *ttyWriter) event(e Event) {
}
last.status = e.Status
last.text = e.Text
last.statusText = e.StatusText
last.details = e.Details
// progress can only go up
if e.Total > last.total {
last.total = e.Total
@@ -160,16 +159,16 @@ func (w *ttyWriter) event(e Event) {
w.tasks[e.ID] = last
} else {
t := task{
ID: e.ID,
parentID: e.ParentID,
startTime: time.Now(),
text: e.Text,
status: e.Status,
statusText: e.StatusText,
current: e.Current,
percent: e.Percent,
total: e.Total,
spinner: NewSpinner(),
ID: e.ID,
parentID: e.ParentID,
startTime: time.Now(),
text: e.Text,
details: e.Details,
status: e.Status,
current: e.Current,
percent: e.Percent,
total: e.Total,
spinner: NewSpinner(),
}
if e.Status == Done || e.Status == Error {
t.stop()
@@ -197,7 +196,7 @@ func (w *ttyWriter) printEvent(e Event) {
case Error:
color = ErrorColor
}
_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, e.Text, color(e.StatusText))
_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details)
}
func (w *ttyWriter) print() {
@@ -228,7 +227,7 @@ func (w *ttyWriter) print() {
var statusPadding int
for _, t := range w.tasks {
l := len(fmt.Sprintf("%s %s", t.ID, t.text))
l := len(t.ID)
if statusPadding < l {
statusPadding = l
}
@@ -314,20 +313,17 @@ func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding in
hideDetails = true
}
var txt string
txt := t.ID
if len(completion) > 0 {
var details string
var progress string
if !hideDetails {
details = fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
progress = fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
}
txt = fmt.Sprintf("%s [%s]%s %s",
txt = fmt.Sprintf("%s [%s]%s",
t.ID,
SuccessColor(strings.Join(completion, "")),
details,
t.text,
progress,
)
} else {
txt = fmt.Sprintf("%s %s", t.ID, t.text)
}
textLen := len(txt)
padding := statusPadding - textLen
@@ -336,19 +332,20 @@ func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding in
}
// calculate the max length for the status text, on errors it
// is 2-3 lines long and breaks the line formatting
maxStatusLen := terminalWidth - textLen - statusPadding - 15
status := t.statusText
maxDetailsLen := terminalWidth - textLen - statusPadding - 15
details := t.details
// in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index
if maxStatusLen > 0 && len(status) > maxStatusLen {
status = status[:maxStatusLen] + "..."
if maxDetailsLen > 0 && len(details) > maxDetailsLen {
details = details[:maxDetailsLen] + "..."
}
text := fmt.Sprintf("%s %s%s %s%s %s",
text := fmt.Sprintf("%s %s%s %s %s%s %s",
pad,
spinner(t),
prefix,
txt,
strings.Repeat(" ", padding),
colorFn(t.status)(status),
colorFn(t.status)(t.text),
details,
)
timer := fmt.Sprintf("%.1fs ", elapsed)
o := align(text, TimerColor(timer), terminalWidth)