Skip to content

Commit f647a7c

Browse files
committed
Experimental terminal UI
Only for `skaffold dev` Signed-off-by: David Gageot <[email protected]>
1 parent 41b51fb commit f647a7c

File tree

7 files changed

+147
-17
lines changed

7 files changed

+147
-17
lines changed

cmd/skaffold/app/cmd/dev.go

+106-4
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ package cmd
1919
import (
2020
"context"
2121
"io"
22+
"strings"
2223

24+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/color"
25+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
2326
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner"
2427
"github.com/pkg/errors"
28+
"github.com/rivo/tview"
2529
"github.com/sirupsen/logrus"
2630
"github.com/spf13/cobra"
2731
)
@@ -33,7 +37,7 @@ func NewCmdDev(out io.Writer) *cobra.Command {
3337
Short: "Runs a pipeline file in development mode",
3438
Args: cobra.NoArgs,
3539
RunE: func(cmd *cobra.Command, args []string) error {
36-
return dev(out)
40+
return dev(out, opts.ExperimentalGUI)
3741
},
3842
}
3943
AddRunDevFlags(cmd)
@@ -44,13 +48,17 @@ func NewCmdDev(out io.Writer) *cobra.Command {
4448
cmd.Flags().IntVarP(&opts.WatchPollInterval, "watch-poll-interval", "i", 1000, "Interval (in ms) between two checks for file changes")
4549
cmd.Flags().BoolVar(&opts.PortForward, "port-forward", true, "Port-forward exposed container ports within pods")
4650
cmd.Flags().StringArrayVarP(&opts.CustomLabels, "label", "l", nil, "Add custom labels to deployed objects. Set multiple times for multiple labels")
51+
cmd.Flags().BoolVar(&opts.ExperimentalGUI, "experimental-gui", true, "Activate an experimental Graphical User Interface")
52+
4753
return cmd
4854
}
4955

50-
func dev(out io.Writer) error {
56+
func dev(out io.Writer, ui bool) error {
5157
ctx, cancel := context.WithCancel(context.Background())
5258
defer cancel()
53-
catchCtrlC(cancel)
59+
if !ui {
60+
catchCtrlC(cancel)
61+
}
5462

5563
cleanup := func() {}
5664
if opts.Cleanup {
@@ -59,6 +67,25 @@ func dev(out io.Writer) error {
5967
}()
6068
}
6169

70+
var (
71+
app *tview.Application
72+
output *config.Output
73+
)
74+
if ui {
75+
app, output = createApp()
76+
defer app.Stop()
77+
78+
go func() {
79+
app.Run()
80+
cancel()
81+
}()
82+
} else {
83+
output = &config.Output{
84+
Main: out,
85+
Logs: out,
86+
}
87+
}
88+
6289
for {
6390
select {
6491
case <-ctx.Done():
@@ -69,7 +96,7 @@ func dev(out io.Writer) error {
6996
return errors.Wrap(err, "creating runner")
7097
}
7198

72-
err = r.Dev(ctx, out, config.Build.Artifacts)
99+
err = r.Dev(ctx, output, config.Build.Artifacts)
73100
if r.HasDeployed() {
74101
cleanup = func() {
75102
if err := r.Cleanup(context.Background(), out); err != nil {
@@ -85,3 +112,78 @@ func dev(out io.Writer) error {
85112
}
86113
}
87114
}
115+
116+
func createApp() (*tview.Application, *config.Output) {
117+
app := tview.NewApplication()
118+
119+
mainView := tview.NewTextView()
120+
mainView.
121+
SetChangedFunc(func() {
122+
app.Draw()
123+
}).
124+
SetDynamicColors(true).
125+
SetBorder(true).
126+
SetTitle("Build")
127+
128+
logsView := tview.NewTextView()
129+
logsView.
130+
SetChangedFunc(func() {
131+
app.Draw()
132+
}).
133+
SetDynamicColors(true).
134+
SetBorder(true).
135+
SetTitle("Logs")
136+
137+
grid := tview.NewGrid()
138+
grid.
139+
SetRows(0, 0).
140+
SetColumns(0).
141+
SetBorders(false).
142+
AddItem(mainView, 0, 0, 1, 1, 0, 0, false).
143+
AddItem(logsView, 1, 0, 1, 1, 0, 0, false)
144+
145+
app.
146+
SetRoot(grid, true).
147+
SetFocus(grid)
148+
149+
output := &config.Output{
150+
Main: color.ColoredWriter{Writer: ansiWriter(mainView)},
151+
Logs: color.ColoredWriter{Writer: ansiWriter(logsView)},
152+
}
153+
154+
return app, output
155+
}
156+
157+
func ansiWriter(writer io.Writer) io.Writer {
158+
return &ansi{
159+
Writer: writer,
160+
replacer: strings.NewReplacer(
161+
"\033[31m", "[maroon]",
162+
"\033[32m", "[green]",
163+
"\033[33m", "[olive]",
164+
"\033[34m", "[navy]",
165+
"\033[35m", "[purple]",
166+
"\033[36m", "[teal]",
167+
"\033[37m", "[silver]",
168+
169+
"\033[91m", "[red]",
170+
"\033[92m", "[lime]",
171+
"\033[93m", "[yellow]",
172+
"\033[94m", "[blue]",
173+
"\033[95m", "[fuchsia]",
174+
"\033[96m", "[aqua]",
175+
"\033[97m", "[white]",
176+
177+
"\033[0m", "",
178+
),
179+
}
180+
}
181+
182+
type ansi struct {
183+
io.Writer
184+
replacer *strings.Replacer
185+
}
186+
187+
func (a *ansi) Write(text []byte) (int, error) {
188+
return a.replacer.WriteString(a.Writer, string(text))
189+
}

docs/content/en/docs/references/cli/_index.md

+2
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ Usage:
205205
Flags:
206206
--cleanup Delete deployments after dev mode is interrupted (default true)
207207
-d, --default-repo string Default repository value (overrides global config)
208+
--experimental-gui Activate an experimental Graphical User Interface (default true)
208209
-f, --filename string Filename or URL to the pipeline file (default "skaffold.yaml")
209210
-l, --label stringArray Add custom labels to deployed objects. Set multiple times for multiple labels
210211
-n, --namespace string Run deployments in the specified namespace
@@ -226,6 +227,7 @@ Env vars:
226227

227228
* `SKAFFOLD_CLEANUP` (same as --cleanup)
228229
* `SKAFFOLD_DEFAULT_REPO` (same as --default-repo)
230+
* `SKAFFOLD_EXPERIMENTAL_GUI` (same as --experimental-gui)
229231
* `SKAFFOLD_FILENAME` (same as --filename)
230232
* `SKAFFOLD_LABEL` (same as --label)
231233
* `SKAFFOLD_NAMESPACE` (same as --namespace)

pkg/skaffold/color/formatter.go

+10
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ var (
5656
Purple = Color(35)
5757
// Cyan can format text to be displayed to the terminal in cyan, using ANSI escape codes.
5858
Cyan = Color(36)
59+
// White can format text to be displayed to the terminal in white, using ANSI escape codes.
60+
White = Color(37)
5961
// None uses ANSI escape codes to reset all formatting.
6062
None = Color(0)
6163

@@ -99,12 +101,20 @@ type ColoredWriteCloser struct {
99101
io.WriteCloser
100102
}
101103

104+
// ColoredWriter forces printing with colors to an io.Writer.
105+
type ColoredWriter struct {
106+
io.Writer
107+
}
108+
102109
// This implementation comes from logrus (https://github.com/sirupsen/logrus/blob/master/terminal_check_notappengine.go),
103110
// unfortunately logrus doesn't expose a public interface we can use to call it.
104111
func isTerminal(w io.Writer) bool {
105112
if _, ok := w.(ColoredWriteCloser); ok {
106113
return true
107114
}
115+
if _, ok := w.(ColoredWriter); ok {
116+
return true
117+
}
108118

109119
switch v := w.(type) {
110120
case *os.File:

pkg/skaffold/config/options.go

+8
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@ limitations under the License.
1717
package config
1818

1919
import (
20+
"io"
2021
"strings"
2122
)
2223

24+
// Output defines which zones on the screen to print to
25+
type Output struct {
26+
Main io.Writer
27+
Logs io.Writer
28+
}
29+
2330
// SkaffoldOptions are options that are set by command line arguments not included
2431
// in the config file itself
2532
type SkaffoldOptions struct {
@@ -30,6 +37,7 @@ type SkaffoldOptions struct {
3037
TailDev bool
3138
PortForward bool
3239
SkipTests bool
40+
ExperimentalGUI bool
3341
Profiles []string
3442
CustomTag string
3543
Namespace string

pkg/skaffold/kubernetes/log.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func (a *LogAggregator) streamRequest(ctx context.Context, headerColor color.Col
192192
if _, err := headerColor.Fprintf(a.output, "%s ", header); err != nil {
193193
return errors.Wrap(err, "writing pod prefix header to out")
194194
}
195-
if _, err := fmt.Fprint(a.output, string(line)); err != nil {
195+
if _, err := color.White.Fprint(a.output, string(line)); err != nil {
196196
return errors.Wrap(err, "writing pod log to out")
197197
}
198198
}

pkg/skaffold/runner/dev.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ package runner
1818

1919
import (
2020
"context"
21-
"io"
2221

2322
"github.com/pkg/errors"
2423
"github.com/sirupsen/logrus"
2524

2625
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build"
2726
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/color"
27+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
2828
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
2929
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
3030
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync"
@@ -36,11 +36,11 @@ var ErrorConfigurationChanged = errors.New("configuration changed")
3636

3737
// Dev watches for changes and runs the skaffold build and deploy
3838
// pipeline until interrupted by the user.
39-
func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*latest.Artifact) error {
40-
logger := r.newLogger(out, artifacts)
39+
func (r *SkaffoldRunner) Dev(ctx context.Context, output *config.Output, artifacts []*latest.Artifact) error {
40+
logger := r.newLogger(output.Logs, artifacts)
4141
defer logger.Stop()
4242

43-
portForwarder := kubernetes.NewPortForwarder(out, r.imageList)
43+
portForwarder := kubernetes.NewPortForwarder(output.Main, r.imageList)
4444
defer portForwarder.Stop()
4545

4646
// Create watcher and register artifacts to build current state of files.
@@ -67,20 +67,20 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
6767
return ErrorConfigurationChanged
6868
case len(changed.needsResync) > 0:
6969
for _, s := range changed.needsResync {
70-
color.Default.Fprintf(out, "Syncing %d files for %s\n", len(s.Copy)+len(s.Delete), s.Image)
70+
color.Default.Fprintf(output.Main, "Syncing %d files for %s\n", len(s.Copy)+len(s.Delete), s.Image)
7171

7272
if err := r.Syncer.Sync(ctx, s); err != nil {
7373
logrus.Warnln("Skipping deploy due to sync error:", err)
7474
return nil
7575
}
7676
}
7777
case len(changed.needsRebuild) > 0:
78-
if err := r.buildTestDeploy(ctx, out, changed.needsRebuild); err != nil {
78+
if err := r.buildTestDeploy(ctx, output.Main, changed.needsRebuild); err != nil {
7979
logrus.Warnln("Skipping deploy due to error:", err)
8080
return nil
8181
}
8282
case changed.needsRedeploy:
83-
if err := r.Deploy(ctx, out, r.builds); err != nil {
83+
if err := r.Deploy(ctx, output.Main, r.builds); err != nil {
8484
logrus.Warnln("Skipping deploy due to error:", err)
8585
return nil
8686
}
@@ -131,7 +131,7 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
131131
}
132132

133133
// First run
134-
if err := r.buildTestDeploy(ctx, out, artifacts); err != nil {
134+
if err := r.buildTestDeploy(ctx, output.Main, artifacts); err != nil {
135135
return errors.Wrap(err, "exiting dev mode because first run failed")
136136
}
137137

@@ -148,5 +148,5 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
148148
}
149149
}
150150

151-
return r.Watcher.Run(ctx, out, onChange)
151+
return r.Watcher.Run(ctx, output.Main, onChange)
152152
}

pkg/skaffold/runner/dev_test.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io/ioutil"
2424
"testing"
2525

26+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
2627
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
2728
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/watch"
2829
"github.com/GoogleContainerTools/skaffold/testutil"
@@ -82,6 +83,13 @@ func (t *TestWatcher) Run(ctx context.Context, out io.Writer, onChange func() er
8283
return nil
8384
}
8485

86+
func discardOutput() *config.Output {
87+
return &config.Output{
88+
Main: ioutil.Discard,
89+
Logs: ioutil.Discard,
90+
}
91+
}
92+
8593
func TestDevFailFirstCycle(t *testing.T) {
8694
var tests = []struct {
8795
description string
@@ -129,7 +137,7 @@ func TestDevFailFirstCycle(t *testing.T) {
129137
runner := createRunner(t, test.testBench)
130138
runner.Watcher = test.watcher
131139

132-
err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{{
140+
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{{
133141
ImageName: "img",
134142
}})
135143

@@ -260,7 +268,7 @@ func TestDev(t *testing.T) {
260268
testBench: test.testBench,
261269
}
262270

263-
err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{
271+
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{
264272
{ImageName: "img1"},
265273
{ImageName: "img2"},
266274
})
@@ -325,7 +333,7 @@ func TestDevSync(t *testing.T) {
325333
testBench: test.testBench,
326334
}
327335

328-
err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{
336+
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{
329337
{
330338
ImageName: "img1",
331339
Sync: map[string]string{

0 commit comments

Comments
 (0)