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 <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2020-04-29 15:08:26 +02:00
parent 678f4018f0
commit 5d61fc119a
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
3 changed files with 210 additions and 6 deletions

View File

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

134
ecs/pkg/console/progress.go Normal file
View File

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

View File

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