Azure backend reports progress

This commit is contained in:
Djordje Lukic 2020-06-17 22:19:08 +02:00
parent fe47237767
commit b55267739a
9 changed files with 295 additions and 168 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/docker/api/azure/login"
"github.com/docker/api/context/store"
"github.com/docker/api/progress"
)
const aciDockerUserAgent = "docker-cli"
@ -47,10 +48,17 @@ func createACIContainers(ctx context.Context, aciContext store.AciContext, group
}
func createOrUpdateACIContainers(ctx context.Context, aciContext store.AciContext, groupDefinition containerinstance.ContainerGroup) error {
w := progress.ContextWriter(ctx)
containerGroupsClient, err := getContainerGroupsClient(aciContext.SubscriptionID)
if err != nil {
return errors.Wrapf(err, "cannot get container group client")
}
w.Event(progress.Event{
ID: *groupDefinition.Name,
Status: progress.Working,
StatusText: "Waiting",
})
future, err := containerGroupsClient.CreateOrUpdate(
ctx,
aciContext.ResourceGroup,
@ -61,14 +69,35 @@ func createOrUpdateACIContainers(ctx context.Context, aciContext store.AciContex
return err
}
w.Event(progress.Event{
ID: *groupDefinition.Name,
Status: progress.Done,
StatusText: "Created",
})
for _, c := range *groupDefinition.Containers {
w.Event(progress.Event{
ID: *c.Name,
Status: progress.Working,
StatusText: "Waiting",
})
}
err = future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
if err != nil {
return err
}
containerGroup, err := future.Result(containerGroupsClient)
if err != nil {
return err
}
for _, c := range *groupDefinition.Containers {
w.Event(progress.Event{
ID: *c.Name,
Status: progress.Done,
StatusText: "Done",
})
}
if len(*containerGroup.Containers) > 1 {
var commands []string

View File

@ -30,11 +30,14 @@ package compose
import (
"context"
"errors"
"os"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"github.com/docker/api/client"
"github.com/docker/api/compose"
"github.com/docker/api/progress"
)
func upCommand() *cobra.Command {
@ -64,5 +67,22 @@ func runUp(ctx context.Context, opts compose.ProjectOptions) error {
return errors.New("compose not implemented in current context")
}
return composeService.Up(ctx, opts)
eg, _ := errgroup.WithContext(ctx)
w, err := progress.NewWriter(os.Stderr)
if err != nil {
return err
}
eg.Go(func() error {
return w.Start(context.Background())
})
ctx = progress.WithContextWriter(ctx, w)
eg.Go(func() error {
defer w.Stop()
err := composeService.Up(ctx, opts)
return err
})
return eg.Wait()
}

View File

@ -30,11 +30,14 @@ package run
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"github.com/docker/api/cli/options/run"
"github.com/docker/api/client"
"github.com/docker/api/progress"
)
// Command runs a container
@ -68,10 +71,23 @@ func runRun(ctx context.Context, image string, opts run.Opts) error {
return err
}
if err = c.ContainerService().Run(ctx, containerConfig); err != nil {
eg, _ := errgroup.WithContext(ctx)
w, err := progress.NewWriter(os.Stderr)
if err != nil {
return err
}
fmt.Println(opts.Name)
eg.Go(func() error {
return w.Start(context.Background())
})
return nil
ctx = progress.WithContextWriter(ctx, w)
eg.Go(func() error {
defer w.Stop()
return c.ContainerService().Run(ctx, containerConfig)
})
err = eg.Wait()
fmt.Println(opts.Name)
return err
}

3
go.mod
View File

@ -6,7 +6,6 @@ require (
github.com/AlecAivazis/survey/v2 v2.0.7
github.com/Azure/azure-sdk-for-go v43.2.0+incompatible
github.com/Azure/azure-storage-file-go v0.7.0
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Azure/go-autorest/autorest v0.10.2
github.com/Azure/go-autorest/autorest/adal v0.8.3
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1
@ -32,6 +31,7 @@ require (
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.4 // indirect
github.com/hashicorp/go-multierror v1.1.0
github.com/moby/term v0.0.0-20200611042045-63b9a826fb74
github.com/morikuni/aec v1.0.0
github.com/onsi/gomega v1.10.1
github.com/opencontainers/go-digest v1.0.0
@ -46,6 +46,7 @@ require (
github.com/tj/survey v2.0.6+incompatible
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect
google.golang.org/grpc v1.29.1
google.golang.org/protobuf v1.24.0

4
go.sum
View File

@ -78,6 +78,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -202,6 +204,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.0.0-20200611042045-63b9a826fb74 h1:kvRIeqJNICemq2UFLx8q/Pj+1IRNZS0XPTaMFkuNsvg=
github.com/moby/term v0.0.0-20200611042045-63b9a826fb74/go.mod h1:pJ0Ot5YGdTcMdxnPMyGCfAr6fKXe0g9cDlz16MuFEBE=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=

29
progress/plain.go Normal file
View File

@ -0,0 +1,29 @@
package progress
import (
"context"
"fmt"
"io"
)
type plainWriter struct {
out io.Writer
done chan bool
}
func (p *plainWriter) Start(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-p.done:
return nil
}
}
func (p *plainWriter) Event(e Event) {
fmt.Println(e.ID, e.Text, e.StatusText)
}
func (p *plainWriter) Stop() {
p.done <- true
}

169
progress/tty.go Normal file
View File

@ -0,0 +1,169 @@
package progress
import (
"context"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/buger/goterm"
"github.com/morikuni/aec"
)
type ttyWriter struct {
out io.Writer
events map[string]Event
eventIDs []string
repeated bool
numLines int
done chan bool
mtx *sync.RWMutex
}
func (w *ttyWriter) Start(ctx context.Context) error {
ticker := time.NewTicker(100 * time.Millisecond)
for {
select {
case <-ctx.Done():
w.print()
return ctx.Err()
case <-w.done:
w.print()
return nil
case <-ticker.C:
w.print()
}
}
}
func (w *ttyWriter) Stop() {
w.done <- true
}
func (w *ttyWriter) Event(e Event) {
w.mtx.Lock()
defer w.mtx.Unlock()
if !contains(w.eventIDs, e.ID) {
w.eventIDs = append(w.eventIDs, e.ID)
}
if _, ok := w.events[e.ID]; ok {
event := w.events[e.ID]
if event.Status != Done && e.Status == Done {
event.stop()
}
event.Status = e.Status
event.Text = e.Text
event.StatusText = e.StatusText
w.events[e.ID] = event
} else {
e.startTime = time.Now()
e.spinner = newSpinner()
w.events[e.ID] = e
}
}
func (w *ttyWriter) print() {
w.mtx.Lock()
defer w.mtx.Unlock()
if len(w.eventIDs) == 0 {
return
}
terminalWidth := goterm.Width()
b := aec.EmptyBuilder
for i := 0; i <= w.numLines; i++ {
b = b.Up(1)
}
if !w.repeated {
b = b.Down(1)
}
w.repeated = true
fmt.Fprint(w.out, b.Column(0).ANSI)
// Hide the cursor while we are printing
fmt.Fprint(w.out, aec.Hide)
defer fmt.Fprint(w.out, aec.Show)
firstLine := fmt.Sprintf("[+] Running %d/%d", numDone(w.events), w.numLines)
if w.numLines != 0 && numDone(w.events) == w.numLines {
firstLine = aec.Apply(firstLine, aec.BlueF)
}
fmt.Fprintln(w.out, firstLine)
var statusPadding int
for _, v := range w.eventIDs {
l := len(fmt.Sprintf("%s %s", w.events[v].ID, w.events[v].Text))
if statusPadding < l {
statusPadding = l
}
}
numLines := 0
for _, v := range w.eventIDs {
line := lineText(w.events[v], terminalWidth, statusPadding)
// nolint: errcheck
fmt.Fprint(w.out, line)
numLines++
}
w.numLines = numLines
}
func lineText(event Event, terminalWidth, statusPadding int) string {
endTime := time.Now()
if event.Status != Working {
endTime = event.endTime
}
elapsed := endTime.Sub(event.startTime).Seconds()
textLen := len(fmt.Sprintf("%s %s", event.ID, event.Text))
padding := statusPadding - textLen
if padding < 0 {
padding = 0
}
text := fmt.Sprintf(" %s %s %s%s %s",
event.spinner.String(),
event.ID,
event.Text,
strings.Repeat(" ", padding),
event.StatusText,
)
timer := fmt.Sprintf("%.1fs\n", elapsed)
o := align(text, timer, terminalWidth)
color := aec.WhiteF
if event.Status == Done {
color = aec.BlueF
}
if event.Status == Error {
color = aec.RedF
}
return aec.Apply(o, color)
}
func numDone(events map[string]Event) int {
i := 0
for _, e := range events {
if e.Status == Done {
i++
}
}
return i
}
func align(l, r string, w int) string {
return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
}
func contains(ar []string, needle string) bool {
for _, v := range ar {
if needle == v {
return true
}
}
return false
}

View File

@ -2,14 +2,11 @@ package progress
import (
"context"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/buger/goterm"
"github.com/morikuni/aec"
"github.com/containerd/console"
"github.com/moby/term"
)
// EventStatus indicates the status of an action
@ -49,16 +46,6 @@ type Writer interface {
Event(Event)
}
type writer struct {
out io.Writer
events map[string]Event
eventIDs []string
repeated bool
numLines int
done chan bool
mtx *sync.RWMutex
}
type writerKey struct{}
// WithContextWriter adds the writer to the context
@ -73,155 +60,27 @@ func ContextWriter(ctx context.Context) Writer {
}
// NewWriter returns a new multi-progress writer
func NewWriter(out io.Writer) Writer {
return &writer{
out: out,
eventIDs: []string{},
events: map[string]Event{},
repeated: false,
done: make(chan bool),
mtx: &sync.RWMutex{},
}
}
func NewWriter(out console.File) (Writer, error) {
_, isTerminal := term.GetFdInfo(out)
func (w *writer) Start(ctx context.Context) error {
ticker := time.NewTicker(100 * time.Millisecond)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-w.done:
w.print()
return nil
case <-ticker.C:
w.print()
if isTerminal {
con, err := console.ConsoleFromFile(out)
if err != nil {
return nil, err
}
return &ttyWriter{
out: con,
eventIDs: []string{},
events: map[string]Event{},
repeated: false,
done: make(chan bool),
mtx: &sync.RWMutex{},
}, nil
}
}
func (w *writer) Stop() {
w.done <- true
}
func (w *writer) Event(e Event) {
w.mtx.Lock()
defer w.mtx.Unlock()
if !contains(w.eventIDs, e.ID) {
w.eventIDs = append(w.eventIDs, e.ID)
}
if _, ok := w.events[e.ID]; ok {
event := w.events[e.ID]
if event.Status != Done && e.Status == Done {
event.stop()
}
event.Status = e.Status
event.Text = e.Text
event.StatusText = e.StatusText
w.events[e.ID] = event
} else {
e.startTime = time.Now()
e.spinner = newSpinner()
w.events[e.ID] = e
}
}
func (w *writer) print() {
w.mtx.Lock()
defer w.mtx.Unlock()
terminalWidth := goterm.Width()
b := aec.EmptyBuilder
for i := 0; i <= w.numLines; i++ {
b = b.Up(1)
}
if !w.repeated {
b = b.Down(1)
}
w.repeated = true
fmt.Fprint(w.out, b.Column(0).ANSI)
// Hide the cursor while we are printing
fmt.Fprint(w.out, aec.Hide)
defer fmt.Fprint(w.out, aec.Show)
firstLine := fmt.Sprintf("[+] Running %d/%d", numDone(w.events), w.numLines)
if w.numLines != 0 && numDone(w.events) == w.numLines {
firstLine = aec.Apply(firstLine, aec.BlueF)
}
fmt.Fprintln(w.out, firstLine)
var statusPadding int
for _, v := range w.eventIDs {
l := len(fmt.Sprintf("%s %s", w.events[v].ID, w.events[v].Text))
if statusPadding < l {
statusPadding = l
}
}
numLines := 0
for _, v := range w.eventIDs {
line := lineText(w.events[v], terminalWidth, statusPadding)
// nolint: errcheck
fmt.Fprint(w.out, line)
numLines++
}
w.numLines = numLines
}
func lineText(event Event, terminalWidth, statusPadding int) string {
endTime := time.Now()
if event.Status != Working {
endTime = event.endTime
}
elapsed := endTime.Sub(event.startTime).Seconds()
textLen := len(fmt.Sprintf("%s %s", event.ID, event.Text))
padding := statusPadding - textLen
if padding < 0 {
padding = 0
}
text := fmt.Sprintf(" %s %s %s%s %s",
event.spinner.String(),
event.ID,
event.Text,
strings.Repeat(" ", padding),
event.StatusText,
)
timer := fmt.Sprintf("%.1fs\n", elapsed)
o := align(text, timer, terminalWidth)
color := aec.WhiteF
if event.Status == Done {
color = aec.BlueF
}
if event.Status == Error {
color = aec.RedF
}
return aec.Apply(o, color)
}
func numDone(events map[string]Event) int {
i := 0
for _, e := range events {
if e.Status == Done {
i++
}
}
return i
}
func align(l, r string, w int) string {
return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
}
func contains(ar []string, needle string) bool {
for _, v := range ar {
if needle == v {
return true
}
}
return false
return &plainWriter{
out: out,
done: make(chan bool),
}, nil
}