From 5d61fc119a5618b4bf5619b8d3ffd58929bf67f7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 29 Apr 2020 15:08:26 +0200 Subject: [PATCH] Format stack events as a set of resources with progress status This mimic how docker-compose report containers creation Signed-off-by: Nicolas De Loof --- ecs/pkg/amazon/up.go | 17 ++-- ecs/pkg/console/progress.go | 134 +++++++++++++++++++++++++++++++ ecs/pkg/console/progress_test.go | 65 +++++++++++++++ 3 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 ecs/pkg/console/progress.go create mode 100644 ecs/pkg/console/progress_test.go diff --git a/ecs/pkg/amazon/up.go b/ecs/pkg/amazon/up.go index 5b964b6eb..c7a504098 100644 --- a/ecs/pkg/amazon/up.go +++ b/ecs/pkg/amazon/up.go @@ -3,11 +3,13 @@ package amazon import ( "context" "fmt" + "sort" + "github.com/aws/aws-sdk-go/aws" cf "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/awslabs/goformation/v4/cloudformation" "github.com/docker/ecs-plugin/pkg/compose" + "github.com/docker/ecs-plugin/pkg/console" ) func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error { @@ -36,23 +38,26 @@ func (c *client) ComposeUp(ctx context.Context, project *compose.Project) error return err } + w := console.NewProgressWriter() known := map[string]struct{}{} err = c.api.WaitStackComplete(ctx, project.Name, func() error { events, err := c.api.DescribeStackEvents(ctx, project.Name) if err != nil { return err } + + sort.Slice(events, func(i, j int) bool { + return events[i].Timestamp.Before(*events[j].Timestamp) + }) + for _, event := range events { if _, ok := known[*event.EventId]; ok { continue } known[*event.EventId] = struct{}{} - description := "-" - if event.ResourceStatusReason != nil { - description = *event.ResourceStatusReason - } - fmt.Printf("%s %q %s %s\n", *event.ResourceType, *event.LogicalResourceId, *event.ResourceStatus, description) + resource := fmt.Sprintf("%s %q", aws.StringValue(event.ResourceType), aws.StringValue(event.LogicalResourceId)) + w.ResourceEvent(resource, aws.StringValue(event.ResourceStatus), aws.StringValue(event.ResourceStatusReason)) } return nil }) diff --git a/ecs/pkg/console/progress.go b/ecs/pkg/console/progress.go new file mode 100644 index 000000000..8ada4df8a --- /dev/null +++ b/ecs/pkg/console/progress.go @@ -0,0 +1,134 @@ +package console + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/sirupsen/logrus" +) + +type resource struct { + name string + status string + details string +} + +type progress struct { + console console + resources []*resource +} + +func NewProgressWriter() *progress { + return &progress{ + console: ansiConsole{os.Stdout}, + } +} + +const ( + cyan = "36;1" + red = "31;1" + green = "32;1" +) + +func (p *progress) ResourceEvent(name string, status string, details string) { + if logrus.IsLevelEnabled(logrus.DebugLevel) { + logrus.Debugf("> %s : %s %s\n", name, status, details) + return + } + p.console.MoveUp(len(p.resources)) + + newResource := true + for _, r := range p.resources { + if r.name == name { + newResource = false + r.status = status + r.details = details + break + } + } + if newResource { + p.resources = append(p.resources, &resource{name, status, details}) + } + + var width int + for _, r := range p.resources { + l := len(r.name) + if width < l { + width = l + } + } + + for _, r := range p.resources { + s := r.status + if strings.HasSuffix(s, "_IN_PROGRESS") { + s = p.console.WiP(s) + } else if strings.HasSuffix(s, "_COMPLETE") { + s = p.console.OK(s) + } else if strings.HasSuffix(s, "_FAILED") { + s = p.console.KO(s) + } + p.console.ClearLine() + p.console.Printf("%-"+strconv.Itoa(width)+"s ... %s %s", r.name, s, r.details) // nolint:errcheck + p.console.MoveDown(1) + } +} + +type console interface { + Printf(format string, a ...interface{}) + MoveUp(int) + MoveDown(int) + ClearLine() + OK(string) string + KO(string) string + WiP(string) string +} + +type ansiConsole struct { + out io.Writer +} + +func (c ansiConsole) Printf(format string, a ...interface{}) { + fmt.Fprintf(c.out, format, a...) // nolint:errcheck + fmt.Fprintf(c.out, "\r") +} + +func (c ansiConsole) MoveUp(i int) { + if i == 0 { + return + } + fmt.Fprintf(c.out, "\033[%dA", i) // nolint:errcheck +} + +func (c ansiConsole) MoveDown(i int) { + if i == 0 { + return + } + fmt.Fprintf(c.out, "\033[%dB", i) // nolint:errcheck +} + +func (c ansiConsole) ClearLine() { + fmt.Fprint(c.out, "\033[2K\r") // nolint:errcheck +} + +func (c ansiConsole) OK(s string) string { + return ansiColor(green, s) +} + +func (c ansiConsole) KO(s string) string { + return ansiColor(red, s) +} + +func (c ansiConsole) WiP(s string) string { + return ansiColor(cyan, s) +} + +func ansiColor(code, s string) string { + return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0")) +} + +func ansi(code string) string { + return fmt.Sprintf("\033[%sm", code) +} diff --git a/ecs/pkg/console/progress_test.go b/ecs/pkg/console/progress_test.go new file mode 100644 index 000000000..552303c2d --- /dev/null +++ b/ecs/pkg/console/progress_test.go @@ -0,0 +1,65 @@ +package console + +import ( + "fmt" + "testing" + + "gotest.tools/v3/assert" +) + +func TestProgressWriter(t *testing.T) { + c := &bufferConsole{} + p := progress{ + console: c, + } + p.ResourceEvent("resource1", "CREATE_IN_PROGRESS", "") + assert.Equal(t, c.lines[0], "resource1 ... CREATE_IN_PROGRESS ") + + p.ResourceEvent("resource2_long_name", "CREATE_IN_PROGRESS", "ok") + assert.Equal(t, c.lines[0], "resource1 ... CREATE_IN_PROGRESS ") + assert.Equal(t, c.lines[1], "resource2_long_name ... CREATE_IN_PROGRESS ok") + + p.ResourceEvent("resource2_long_name", "CREATE_COMPLETE", "done") + assert.Equal(t, c.lines[0], "resource1 ... CREATE_IN_PROGRESS ") + assert.Equal(t, c.lines[1], "resource2_long_name ... CREATE_COMPLETE done") + + p.ResourceEvent("resource1", "CREATE_FAILED", "oups") + assert.Equal(t, c.lines[0], "resource1 ... CREATE_FAILED oups") + assert.Equal(t, c.lines[1], "resource2_long_name ... CREATE_COMPLETE done") +} + +type bufferConsole struct { + pos int + lines []string +} + +func (b *bufferConsole) Printf(format string, a ...interface{}) { + b.lines[b.pos] = fmt.Sprintf(format, a...) +} + +func (b *bufferConsole) MoveUp(i int) { + b.pos -= i +} + +func (b *bufferConsole) MoveDown(i int) { + b.pos += i +} + +func (b *bufferConsole) ClearLine() { + if len(b.lines) <= b.pos { + b.lines = append(b.lines, "") + } + b.lines[b.pos] = "" +} + +func (b *bufferConsole) OK(s string) string { + return s +} + +func (b *bufferConsole) KO(s string) string { + return s +} + +func (b *bufferConsole) WiP(s string) string { + return s +}