gokrazy/build: show import-snapshot progress, quiet in CI
Capture the AWS Progress/StatusMessage fields and report them instead of reprinting the full describe-import-snapshot-tasks JSON every 5s. On a terminal, repaint one live percentage line; otherwise log one line per phase change so CI stays terse. Updates #1866 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
committed by
Kristoffer Dalby
parent
7653a1e438
commit
b049ce71a5
+56
-28
@@ -26,6 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"tailscale.com/gokrazy/mkfs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -354,6 +355,8 @@ func (b *Builder) startImportSnapshot(ctx context.Context) (importTaskID string,
|
||||
*/
|
||||
|
||||
func (b *Builder) waitForImportSnapshot(ctx context.Context, importTaskID string) (snapID string, err error) {
|
||||
interactive := isTerminal(b.Stderr)
|
||||
var lastPhase string // last (status/message) reported in non-interactive mode
|
||||
for {
|
||||
out, err := b.awsCmd(ctx, "ec2", "describe-import-snapshot-tasks", "--import-task-ids", importTaskID).CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -363,47 +366,47 @@ func (b *Builder) waitForImportSnapshot(ctx context.Context, importTaskID string
|
||||
var resp struct {
|
||||
ImportSnapshotTasks []struct {
|
||||
SnapshotTaskDetail struct {
|
||||
SnapshotID string `json:"SnapshotId"`
|
||||
Status string `json:"Status"`
|
||||
SnapshotID string `json:"SnapshotId"`
|
||||
Status string `json:"Status"`
|
||||
StatusMessage string `json:"StatusMessage"`
|
||||
Progress string `json:"Progress"`
|
||||
} `json:"SnapshotTaskDetail"`
|
||||
} `json:"ImportSnapshotTasks"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &resp); err != nil {
|
||||
return "", fmt.Errorf("unmarshal response: %v: %s", err, out)
|
||||
}
|
||||
var d struct {
|
||||
SnapshotID string `json:"SnapshotId"`
|
||||
Status string `json:"Status"`
|
||||
StatusMessage string `json:"StatusMessage"`
|
||||
Progress string `json:"Progress"`
|
||||
}
|
||||
if len(resp.ImportSnapshotTasks) > 0 {
|
||||
first := &resp.ImportSnapshotTasks[0]
|
||||
if first.SnapshotTaskDetail.Status == "completed" {
|
||||
return first.SnapshotTaskDetail.SnapshotID, nil
|
||||
d = resp.ImportSnapshotTasks[0].SnapshotTaskDetail
|
||||
}
|
||||
if d.Status == "completed" {
|
||||
if interactive {
|
||||
fmt.Fprintln(b.Stderr) // move off the live progress line
|
||||
}
|
||||
return d.SnapshotID, nil
|
||||
}
|
||||
b.logf("Still waiting; got: %s", out)
|
||||
|
||||
// TODO(bradfitz): percentage bar?
|
||||
// Looks like:
|
||||
/* 2024/05/14 13:03:21 Still waiting; got: {
|
||||
"ImportSnapshotTasks": [
|
||||
{
|
||||
"ImportTaskId": "import-snap-0232251d0fbcb33fd",
|
||||
"SnapshotTaskDetail": {
|
||||
"DiskImageSize": 1258299392.0,
|
||||
"Format": "RAW",
|
||||
"Progress": "32",
|
||||
"Status": "active",
|
||||
"StatusMessage": "validated",
|
||||
"Url": "s3://tskrazy-import/tskrazy.img",
|
||||
"UserBucket": {
|
||||
"S3Bucket": "tskrazy-import",
|
||||
"S3Key": "tskrazy.img"
|
||||
}
|
||||
},
|
||||
"Tags": []
|
||||
}
|
||||
]
|
||||
}*/
|
||||
if interactive {
|
||||
// Repaint one line in place each poll.
|
||||
fmt.Fprintf(b.Stderr, "\r\x1b[K%s", importProgressLine(d.Status, d.StatusMessage, d.Progress))
|
||||
} else if phase := d.Status + "/" + d.StatusMessage; phase != lastPhase {
|
||||
// Non-interactive (CI, pipe): one line per phase change only,
|
||||
// not a fresh line every poll.
|
||||
lastPhase = phase
|
||||
b.logf("%s", importProgressLine(d.Status, d.StatusMessage, d.Progress))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if interactive {
|
||||
fmt.Fprintln(b.Stderr)
|
||||
}
|
||||
return "", ctx.Err()
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
@@ -480,6 +483,31 @@ func imageSizeBytesFor(app string) int64 {
|
||||
return n
|
||||
}
|
||||
|
||||
// importProgressLine formats a one-line status for an in-progress EC2
|
||||
// import-snapshot task from its Status, StatusMessage, and Progress (a
|
||||
// percentage string like "32") fields, any of which may be empty early on.
|
||||
func importProgressLine(status, statusMessage, progress string) string {
|
||||
msg := statusMessage
|
||||
if msg == "" {
|
||||
msg = status
|
||||
}
|
||||
if msg == "" {
|
||||
msg = "pending"
|
||||
}
|
||||
if progress == "" {
|
||||
return "importing snapshot: " + msg
|
||||
}
|
||||
return fmt.Sprintf("importing snapshot: %s%% (%s)", progress, msg)
|
||||
}
|
||||
|
||||
// isTerminal reports whether w writes to an interactive terminal, so
|
||||
// callers can show a live-updating progress line instead of a stream of
|
||||
// separate log lines.
|
||||
func isTerminal(w io.Writer) bool {
|
||||
f, ok := w.(*os.File)
|
||||
return ok && isatty.IsTerminal(f.Fd())
|
||||
}
|
||||
|
||||
// awsArch maps a Go GOARCH to the AWS EC2 --architecture value.
|
||||
func awsArch(goarch string) string {
|
||||
switch goarch {
|
||||
|
||||
@@ -54,6 +54,26 @@ func TestBuildCapturesError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportProgressLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, status, statusMessage, progress, want string
|
||||
}{
|
||||
{"early-empty", "", "", "", "importing snapshot: pending"},
|
||||
{"status-only", "active", "", "", "importing snapshot: active"},
|
||||
{"message-no-progress", "active", "pending", "", "importing snapshot: pending"},
|
||||
{"full", "active", "validated", "32", "importing snapshot: 32% (validated)"},
|
||||
{"message-beats-status", "active", "converting", "80", "importing snapshot: 80% (converting)"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := importProgressLine(tt.status, tt.statusMessage, tt.progress); got != tt.want {
|
||||
t.Errorf("importProgressLine(%q, %q, %q) = %q; want %q",
|
||||
tt.status, tt.statusMessage, tt.progress, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMINameFrom(t *testing.T) {
|
||||
const now = 1720000000
|
||||
tests := []struct {
|
||||
|
||||
Reference in New Issue
Block a user