Просмотр исходного кода

stop progress UI during build to prevent interference with buildkit Display

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 2 месяцев назад
Родитель
Сommit
a8933c91e7

+ 2 - 2
pkg/compose/build.go

@@ -226,7 +226,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 		if err != nil {
 			return err
 		}
-		s.events.On(progress.BuildingEvent("Image " + buildOptions.Tags[0]))
+		s.events.On(progress.BuildingEvent(buildOptions.Tags[0]))
 
 		trace.SpanFromContext(ctx).SetAttributes(attribute.String("builder", "buildkit"))
 		digest, err := s.doBuildBuildkit(ctx, name, buildOptions, w, nodes)
@@ -256,7 +256,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 			service := project.Services[names[i]]
 			imageRef := api.GetImageNameOrDefault(service, project.Name)
 			imageIDs[imageRef] = imageDigest
-			s.events.On(progress.BuiltEvent("Image " + imageRef))
+			s.events.On(progress.BuiltEvent(imageRef))
 		}
 	}
 	return imageIDs, err

+ 3 - 1
pkg/compose/build_bake.go

@@ -226,6 +226,8 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
 		}
 
 		image := api.GetImageNameOrDefault(service, project.Name)
+		s.events.On(progress.BuildingEvent(image))
+
 		expectedImages[serviceName] = image
 
 		pull := service.Build.Pull || options.Pull
@@ -426,7 +428,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 " + image))
+		s.events.On(progress.BuiltEvent(image))
 	}
 	return results, nil
 }

+ 2 - 2
pkg/compose/build_classic.go

@@ -184,7 +184,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj
 
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
-	s.events.On(progress2.BuildingEvent("Image " + imageName))
+	s.events.On(progress2.BuildingEvent(imageName))
 	response, err := s.apiClient().ImageBuild(ctx, body, buildOpts)
 	if err != nil {
 		return "", err
@@ -213,7 +213,7 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj
 		}
 		return "", err
 	}
-	s.events.On(progress2.BuiltEvent("Image " + imageName))
+	s.events.On(progress2.BuiltEvent(imageName))
 	return imageID, nil
 }
 

+ 6 - 6
pkg/compose/model.go

@@ -145,18 +145,18 @@ func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quiet
 		events.On(progress.ErrorMessageEvent(model.Name, err.Error()))
 	}
 	events.On(progress.Event{
-		ID:     model.Name,
-		Status: progress.Working,
-		Text:   "Pulled",
+		ID:         model.Name,
+		Status:     progress.Working,
+		StatusText: "Pulled",
 	})
 	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,
-		Text:   "Configuring",
+		ID:         config.Name,
+		Status:     progress.Working,
+		StatusText: "Configuring",
 	})
 	// configure [--context-size=<n>] MODEL [-- <runtime-flags...>]
 	args := []string{"configure"}

+ 2 - 10
pkg/compose/pull.go

@@ -174,11 +174,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.Event{
-		ID:     resource,
-		Status: progress.Working,
-		Text:   "Pulling",
-	})
+	s.events.On(progress.PullingEvent(service.Image))
 	ref, err := reference.ParseNormalizedNamed(service.Image)
 	if err != nil {
 		return "", err
@@ -246,11 +242,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
 			toPullProgressEvent(resource, jm, s.events)
 		}
 	}
-	s.events.On(progress.Event{
-		ID:     resource,
-		Status: progress.Done,
-		Text:   "Pulled",
-	})
+	s.events.On(progress.PulledEvent(service.Image))
 
 	inspected, err := s.apiClient().ImageInspect(ctx, service.Image)
 	if err != nil {

+ 2 - 2
pkg/e2e/compose_run_test.go

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

+ 5 - 5
pkg/e2e/pull_test.go

@@ -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) {
@@ -54,7 +54,7 @@ func TestComposePull(t *testing.T) {
 
 		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, "latest Pulled"))
+		assert.Assert(t, strings.Contains(output, "alpine:latest  Pulled"))
 	})
 
 	t.Run("Verify skipped no image to be pulled", func(t *testing.T) {

+ 53 - 19
pkg/progress/event.go

@@ -32,6 +32,30 @@ const (
 	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"
+)
+
 // Event represents a progress event.
 type Event struct {
 	ID         string
@@ -51,97 +75,107 @@ func ErrorMessageEvent(id string, msg string) Event {
 
 // ErrorEvent creates a new Error Event
 func ErrorEvent(id string) Event {
-	return NewEvent(id, Error, "Error")
+	return NewEvent(id, Error, StatusError)
 }
 
 // CreatingEvent creates a new Create in progress Event
 func CreatingEvent(id string) Event {
-	return NewEvent(id, Working, "Creating")
+	return NewEvent(id, Working, StatusCreating)
 }
 
 // StartingEvent creates a new Starting in progress Event
 func StartingEvent(id string) Event {
-	return NewEvent(id, Working, "Starting")
+	return NewEvent(id, Working, StatusStarting)
 }
 
 // StartedEvent creates a new Started in progress Event
 func StartedEvent(id string) Event {
-	return NewEvent(id, Done, "Started")
+	return NewEvent(id, Done, StatusStarted)
 }
 
 // Waiting creates a new waiting event
 func Waiting(id string) Event {
-	return NewEvent(id, Working, "Waiting")
+	return NewEvent(id, Working, StatusWaiting)
 }
 
 // Healthy creates a new healthy event
 func Healthy(id string) Event {
-	return NewEvent(id, Done, "Healthy")
+	return NewEvent(id, Done, StatusHealthy)
 }
 
 // Exited creates a new exited event
 func Exited(id string) Event {
-	return NewEvent(id, Done, "Exited")
+	return NewEvent(id, Done, StatusExited)
 }
 
 // RestartingEvent creates a new Restarting in progress Event
 func RestartingEvent(id string) Event {
-	return NewEvent(id, Working, "Restarting")
+	return NewEvent(id, Working, StatusRestarting)
 }
 
 // RestartedEvent creates a new Restarted in progress Event
 func RestartedEvent(id string) Event {
-	return NewEvent(id, Done, "Restarted")
+	return NewEvent(id, Done, StatusRestarted)
 }
 
 // RunningEvent creates a new Running in progress Event
 func RunningEvent(id string) Event {
-	return NewEvent(id, Done, "Running")
+	return NewEvent(id, Done, StatusRunning)
 }
 
 // CreatedEvent creates a new Created (done) Event
 func CreatedEvent(id string) Event {
-	return NewEvent(id, Done, "Created")
+	return NewEvent(id, Done, StatusCreated)
 }
 
 // StoppingEvent creates a new Stopping in progress Event
 func StoppingEvent(id string) Event {
-	return NewEvent(id, Working, "Stopping")
+	return NewEvent(id, Working, StatusStopping)
 }
 
 // StoppedEvent creates a new Stopping in progress Event
 func StoppedEvent(id string) Event {
-	return NewEvent(id, Done, "Stopped")
+	return NewEvent(id, Done, StatusStopped)
 }
 
 // KillingEvent creates a new Killing in progress Event
 func KillingEvent(id string) Event {
-	return NewEvent(id, Working, "Killing")
+	return NewEvent(id, Working, StatusKilling)
 }
 
 // KilledEvent creates a new Killed in progress Event
 func KilledEvent(id string) Event {
-	return NewEvent(id, Done, "Killed")
+	return NewEvent(id, Done, StatusKilled)
 }
 
 // RemovingEvent creates a new Removing in progress Event
 func RemovingEvent(id string) Event {
-	return NewEvent(id, Working, "Removing")
+	return NewEvent(id, Working, StatusRemoving)
 }
 
 // RemovedEvent creates a new removed (done) Event
 func RemovedEvent(id string) Event {
-	return NewEvent(id, Done, "Removed")
+	return NewEvent(id, Done, StatusRemoved)
 }
 
 // BuildingEvent creates a new Building in progress Event
 func BuildingEvent(id string) Event {
-	return NewEvent(id, Working, "Building")
+	return NewEvent("Image "+id, Working, StatusBuilding)
 }
 
 // BuiltEvent creates a new built (done) Event
 func BuiltEvent(id string) Event {
-	return NewEvent(id, Done, "Built")
+	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

+ 14 - 21
pkg/progress/tty.go

@@ -20,7 +20,6 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -38,7 +37,6 @@ func NewTTYWriter(out io.Writer) EventProcessor {
 	return &ttyWriter{
 		out:   out,
 		tasks: map[string]task{},
-		ids:   []string{},
 		done:  make(chan bool),
 		mtx:   &sync.Mutex{},
 	}
@@ -47,7 +45,6 @@ func NewTTYWriter(out io.Writer) EventProcessor {
 type ttyWriter struct {
 	out             io.Writer
 	tasks           map[string]task
-	ids             []string
 	repeated        bool
 	numLines        int
 	done            chan bool
@@ -122,11 +119,14 @@ func (w *ttyWriter) On(events ...Event) {
 }
 
 func (w *ttyWriter) event(e Event) {
-	if !slices.Contains(w.ids, e.ID) {
-		w.ids = append(w.ids, e.ID)
+	// Suspend print while a build is in progress, to avoid collision with buildkit Display
+	if e.StatusText == StatusBuilding {
+		w.ticker.Stop()
+	} else {
+		w.ticker.Reset(100 * time.Millisecond)
 	}
-	if _, ok := w.tasks[e.ID]; ok {
-		last := w.tasks[e.ID]
+
+	if last, ok := w.tasks[e.ID]; ok {
 		switch e.Status {
 		case Done, Error, Warning:
 			if last.status != e.Status {
@@ -194,10 +194,10 @@ func (w *ttyWriter) printEvent(e Event) {
 	_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, e.Text, color(e.StatusText))
 }
 
-func (w *ttyWriter) print() { //nolint:gocyclo
+func (w *ttyWriter) print() {
 	w.mtx.Lock()
 	defer w.mtx.Unlock()
-	if len(w.ids) == 0 {
+	if len(w.tasks) == 0 {
 		return
 	}
 	terminalWidth := goterm.Width()
@@ -218,14 +218,10 @@ func (w *ttyWriter) print() { //nolint:gocyclo
 	}()
 
 	firstLine := fmt.Sprintf("[+] %s %d/%d", w.operation, numDone(w.tasks), len(w.tasks))
-	if w.numLines != 0 && numDone(w.tasks) == w.numLines {
-		firstLine = DoneColor(firstLine)
-	}
 	_, _ = fmt.Fprintln(w.out, firstLine)
 
 	var statusPadding int
-	for _, v := range w.ids {
-		t := w.tasks[v]
+	for _, t := range w.tasks {
 		l := len(fmt.Sprintf("%s %s", t.ID, t.text))
 		if statusPadding < l {
 			statusPadding = l
@@ -235,20 +231,18 @@ func (w *ttyWriter) print() { //nolint:gocyclo
 		}
 	}
 
-	if len(w.ids) > goterm.Height()-2 {
+	if len(w.tasks) > goterm.Height()-2 {
 		w.skipChildEvents = true
 	}
 	numLines := 0
-	for _, v := range w.ids {
-		t := w.tasks[v]
+	for _, t := range w.tasks {
 		if t.parentID != "" {
 			continue
 		}
 		line := w.lineText(t, "", terminalWidth, statusPadding, w.dryRun)
 		_, _ = fmt.Fprint(w.out, line)
 		numLines++
-		for _, v := range w.ids {
-			t := w.tasks[v]
+		for _, t := range w.tasks {
 			if t.parentID == t.ID {
 				if w.skipChildEvents {
 					continue
@@ -292,8 +286,7 @@ 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 {
-		for _, v := range w.ids {
-			child := w.tasks[v]
+		for _, child := range w.tasks {
 			if child.parentID == parent.ID {
 				if child.status == Working && child.total == 0 {
 					// we don't have totals available for all the child events