Skip to content

Commit 9be7c0d

Browse files
feat: improve finalizing step feedback (#498)
* feat: improve finalizing step feedback provide users with more feedback during the finalizing step of the installation. shows the images being pushed. * chore: adjusting user feedback * chore: fix 'make clean' and bumps kots version * chore: fix messages for online installations * chore: working on airgap messaging * chore: working on airgap messaging * chore: working on airgap messaging * chore: working on airgap messaging * chore: working on airgap messaging * chore: working on airgap messaging * chore: fixed tests * chore: working on airgap messaging * chore: working on airgap messaging * chore: working on airgap messaging
1 parent 534c0e3 commit 9be7c0d

File tree

5 files changed

+150
-30
lines changed

5 files changed

+150
-30
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ e2e-test:
149149
clean:
150150
rm -rf output
151151
rm -rf pkg/goods/bins
152+
rm -rf pkg/goods/internal/bins
152153

153154
.PHONY: lint
154155
lint:

pkg/addons/adminconsole/adminconsole.go

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"encoding/base64"
88
"fmt"
99
"os"
10+
"regexp"
11+
"strings"
1012
"time"
1113

1214
"github.com/k0sproject/dig"
@@ -41,6 +43,7 @@ var (
4143
Version = "v0.0.0"
4244
ImageOverride = ""
4345
MigrationsImageOverride = ""
46+
CounterRegex = regexp.MustCompile(`(\d+)/(\d+)`)
4447
)
4548

4649
// protectedFields are helm values that are not overwritten when upgrading the addon.
@@ -207,6 +210,99 @@ func (a *AdminConsole) GetAdditionalImages() []string {
207210
return nil
208211
}
209212

213+
// MaskKotsOutputForOnline masks the kots cli output during online installations. For
214+
// online installations we only want to print "Finalizing" until it is done and then
215+
// print "Finished!".
216+
func (a *AdminConsole) MaskKotsOutputForOnline() spinner.MaskFn {
217+
return func(message string) string {
218+
if strings.Contains(message, "Finished") {
219+
return message
220+
}
221+
return "Finalizing"
222+
}
223+
}
224+
225+
// MaskKotsOutputForAirgap masks the kots cli output during airgap installations. This
226+
// function replaces some of the messages being printed to the user so the output looks
227+
// nicer.
228+
func (a *AdminConsole) MaskKotsOutputForAirgap() spinner.MaskFn {
229+
current := "Uploading air gap bundle"
230+
return func(message string) string {
231+
switch {
232+
case strings.Contains(message, "Pushing application images"):
233+
current = message
234+
case strings.Contains(message, "Pushing embedded cluster artifacts"):
235+
current = message
236+
case strings.Contains(message, "Waiting for Admin Console"):
237+
current = "Finalizing"
238+
case strings.Contains(message, "Finished!"):
239+
current = message
240+
}
241+
return current
242+
}
243+
}
244+
245+
// KostsOutputLineBreaker creates a line break (new spinner) when some of the messages
246+
// are printed to the user. For example: after finishing all image uploads we want to
247+
// have a new spinner for the artifacts upload.
248+
func (a *AdminConsole) KostsOutputLineBreaker() spinner.LineBreakerFn {
249+
// finished is an auxiliary function that evaluates if a message refers to a
250+
// step that has been finished. We determine that by inspected if the message
251+
// contains %d/%d and both integers are equal.
252+
finished := func(message string) bool {
253+
matches := CounterRegex.FindStringSubmatch(message)
254+
if len(matches) != 3 {
255+
return false
256+
}
257+
var counter int
258+
if _, err := fmt.Sscanf(matches[1], "%d", &counter); err != nil {
259+
return false
260+
}
261+
var total int
262+
if _, err := fmt.Sscanf(matches[2], "%d", &total); err != nil {
263+
return false
264+
}
265+
return counter == total
266+
}
267+
268+
var previous string
269+
var seen = map[string]bool{}
270+
return func(current string) (bool, string) {
271+
defer func() {
272+
previous = current
273+
}()
274+
275+
// if we have already seen this message we certainly have already assessed
276+
// if a break line as necessary or not, on this case we return false so we
277+
// do not keep breaking lines indefinitely.
278+
if _, ok := seen[current]; ok {
279+
return false, ""
280+
}
281+
seen[current] = true
282+
283+
// if the previous message evaluated does not relate to an end of a process
284+
// we don't want to break the line. i.e. we only want to break the line when
285+
// the previous evaluated message contains %d/%d and both integers are equal.
286+
if !finished(previous) {
287+
return false, ""
288+
}
289+
290+
// if we are printing a message about pushing the embedded cluster artifacts
291+
// it means that we have finished with the images and we want to break the line.
292+
if strings.Contains(current, "Pushing embedded cluster artifacts") {
293+
return true, "Application images are ready!"
294+
}
295+
296+
// if we are printing a message about the finalization of the installation it
297+
// means that the embedded cluster artifacts are ready and we want to break the
298+
// line.
299+
if current == "Finalizing" {
300+
return true, "Embedded cluster artifacts are ready!"
301+
}
302+
return false, ""
303+
}
304+
}
305+
210306
// Outro waits for the adminconsole to be ready.
211307
func (a *AdminConsole) Outro(ctx context.Context, cli client.Client) error {
212308
loading := spinner.Start()
@@ -244,28 +340,27 @@ func (a *AdminConsole) Outro(ctx context.Context, cli client.Client) error {
244340
return fmt.Errorf("error waiting for admin console: %v", lasterr)
245341
}
246342

343+
loading.Closef("Admin Console is ready!")
247344
if a.licenseFile == "" {
248-
loading.Closef("Admin Console is ready!")
249345
return nil
250346
}
251347

252-
loading.Infof("Finalizing")
253-
254348
kotsBinPath, err := goods.MaterializeInternalBinary("kubectl-kots")
255349
if err != nil {
256-
loading.Close()
257350
return fmt.Errorf("unable to materialize kubectl-kots binary: %w", err)
258351
}
259352
defer os.Remove(kotsBinPath)
260353

261354
license, err := helpers.ParseLicense(a.licenseFile)
262355
if err != nil {
356+
loading.CloseWithError()
263357
return fmt.Errorf("unable to parse license: %w", err)
264358
}
265359

266360
var appVersionLabel string
267361
var channelSlug string
268362
if channelRelease, err := release.GetChannelRelease(); err != nil {
363+
loading.CloseWithError()
269364
return fmt.Errorf("unable to get channel release: %w", err)
270365
} else if channelRelease != nil {
271366
appVersionLabel = channelRelease.VersionLabel
@@ -277,6 +372,8 @@ func (a *AdminConsole) Outro(ctx context.Context, cli client.Client) error {
277372
upstreamURI = fmt.Sprintf("%s/%s", upstreamURI, channelSlug)
278373
}
279374

375+
var lbreakfn spinner.LineBreakerFn
376+
maskfn := a.MaskKotsOutputForOnline()
280377
installArgs := []string{
281378
"install",
282379
upstreamURI,
@@ -290,14 +387,17 @@ func (a *AdminConsole) Outro(ctx context.Context, cli client.Client) error {
290387
}
291388
if a.airgapBundle != "" {
292389
installArgs = append(installArgs, "--airgap-bundle", a.airgapBundle)
390+
maskfn = a.MaskKotsOutputForAirgap()
391+
lbreakfn = a.KostsOutputLineBreaker()
293392
}
294393

295-
if _, err := helpers.RunCommand(kotsBinPath, installArgs...); err != nil {
296-
loading.Close()
394+
loading = spinner.Start(spinner.WithMask(maskfn), spinner.WithLineBreaker(lbreakfn))
395+
if err := helpers.RunCommandWithWriter(loading, kotsBinPath, installArgs...); err != nil {
396+
loading.CloseWithError()
297397
return fmt.Errorf("unable to install the application: %w", err)
298398
}
299399

300-
loading.Closef("Admin Console is ready!")
400+
loading.Closef("Finished!")
301401
a.printSuccessMessage(license.Spec.AppSlug)
302402
return nil
303403
}

pkg/helpers/command.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,40 @@ package helpers
33
import (
44
"bytes"
55
"fmt"
6+
"io"
67
"os/exec"
78

89
"github.com/sirupsen/logrus"
910
)
1011

11-
// RunCommand spawns a command and capture its output. Outputs are logged using the
12-
// logrus package and stdout is returned as a string.
13-
func RunCommand(bin string, args ...string) (string, error) {
12+
// RunCommandWithWriter runs a the provided command. The stdout of the command is
13+
// written to the provider writer.
14+
func RunCommandWithWriter(to io.Writer, bin string, args ...string) error {
1415
fullcmd := append([]string{bin}, args...)
1516
logrus.Debugf("running command: %v", fullcmd)
1617

17-
stdout := bytes.NewBuffer(nil)
1818
stderr := bytes.NewBuffer(nil)
19+
stdout := bytes.NewBuffer(nil)
1920
cmd := exec.Command(bin, args...)
20-
cmd.Stdout = stdout
21+
cmd.Stdout = io.MultiWriter(to, stdout)
2122
cmd.Stderr = stderr
2223
if err := cmd.Run(); err != nil {
2324
logrus.Debugf("failed to run command:")
2425
logrus.Debugf("stdout: %s", stdout.String())
2526
logrus.Debugf("stderr: %s", stderr.String())
2627
if stderr.String() != "" {
27-
return "", fmt.Errorf("%w: %s", err, stderr.String())
28+
return fmt.Errorf("%w: %s", err, stderr.String())
2829
}
30+
return err
31+
}
32+
return nil
33+
}
34+
35+
// RunCommand spawns a command and capture its output. Outputs are logged using the
36+
// logrus package and stdout is returned as a string.
37+
func RunCommand(bin string, args ...string) (string, error) {
38+
stdout := bytes.NewBuffer(nil)
39+
if err := RunCommandWithWriter(stdout, bin, args...); err != nil {
2940
return "", err
3041
}
3142
return stdout.String(), nil

pkg/spinner/spinner.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ var blocks = []string{"◐", "◓", "◑", "◒"}
1313
type WriteFn func(string, ...any) (int, error)
1414

1515
// LineBreakerFn is a function that determines if it is time to break the
16-
// line thus creating a new spinner line. The previous step is flagged as
17-
// success.
18-
type LineBreakerFn func(string) bool
16+
// line thus creating a new spinner line. Returns a boolean and a string
17+
// indicating what needs to be printed before the line break.
18+
type LineBreakerFn func(string) (bool, string)
1919

2020
// MaskFn is a function that masks a message. Receives a string and
2121
// returns a string, the returned string is printed to the terminal.
@@ -109,20 +109,22 @@ func (m *MessageWriter) loop() {
109109
counter++
110110
}
111111

112-
var lbreak bool
113-
if m.lbreak != nil && previous != message {
114-
lbreak = m.lbreak(message)
115-
}
116-
117112
if m.mask != nil {
118113
message = m.mask(message)
119114
}
120115

116+
if m.lbreak != nil && previous != message {
117+
if lbreak, lcontent := m.lbreak(message); lbreak {
118+
if diff := len(previous) - len(lcontent); diff > 0 {
119+
suffix := strings.Repeat(" ", diff)
120+
lcontent = fmt.Sprintf("%s%s", lcontent, suffix)
121+
}
122+
m.printf("\033[K\r✔ %s\n", lcontent)
123+
}
124+
}
125+
121126
pos := counter % len(blocks)
122127
if !end {
123-
if lbreak {
124-
m.printf("\033[K\r✔ %s\n", previous)
125-
}
126128
m.printf("\033[K\r%s %s", blocks[pos], message)
127129
continue
128130
}

pkg/spinner/spinner_test.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,14 @@ func TestMask(t *testing.T) {
111111

112112
func TestLineBreak(t *testing.T) {
113113
buf := bytes.NewBuffer(nil)
114-
lbreak := func(s string) bool {
115-
return s == "test 3" || s == "test 8"
114+
lbreak := func(s string) (bool, string) {
115+
if s == "test 3" {
116+
return true, "ping 2"
117+
}
118+
if s == "test 8" {
119+
return true, "ping 7"
120+
}
121+
return false, ""
116122
}
117123
pb := Start(
118124
WithWriter(WriteTo(buf)),
@@ -123,11 +129,11 @@ func TestLineBreak(t *testing.T) {
123129
}
124130
pb.Close()
125131
// we expect the following output:
126-
// ✔ test 2 (\n)
127-
// ✔ test 7 (\n)
132+
// ✔ ping 2 (\n)
133+
// ✔ ping 7 (\n)
128134
// ✔ test 99 (\n)
129135
assert.Equal(t, strings.Count(buf.String(), "\n"), 3)
130-
assert.Contains(t, buf.String(), "test 2")
131-
assert.Contains(t, buf.String(), "test 7")
136+
assert.Contains(t, buf.String(), "ping 2")
137+
assert.Contains(t, buf.String(), "ping 7")
132138
assert.Contains(t, buf.String(), "test 99")
133139
}

0 commit comments

Comments
 (0)