Skip to content

Commit

Permalink
Experimental terminal UI
Browse files Browse the repository at this point in the history
Only for `skaffold dev`

Signed-off-by: David Gageot <david@gageot.net>
  • Loading branch information
dgageot committed Feb 5, 2019
1 parent e119300 commit 5a8bdcf
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 17 deletions.
110 changes: 106 additions & 4 deletions cmd/skaffold/app/cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ package cmd
import (
"context"
"io"
"strings"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/color"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner"
"github.com/pkg/errors"
"github.com/rivo/tview"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
Expand All @@ -34,7 +38,7 @@ func NewCmdDev(out io.Writer) *cobra.Command {
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts.Command = "dev"
return dev(out)
return dev(out, opts.ExperimentalGUI)
},
}
AddRunDevFlags(cmd)
Expand All @@ -45,13 +49,17 @@ func NewCmdDev(out io.Writer) *cobra.Command {
cmd.Flags().IntVarP(&opts.WatchPollInterval, "watch-poll-interval", "i", 1000, "Interval (in ms) between two checks for file changes")
cmd.Flags().BoolVar(&opts.PortForward, "port-forward", true, "Port-forward exposed container ports within pods")
cmd.Flags().StringArrayVarP(&opts.CustomLabels, "label", "l", nil, "Add custom labels to deployed objects. Set multiple times for multiple labels")
cmd.Flags().BoolVar(&opts.ExperimentalGUI, "experimental-gui", false, "Experimental Graphical User Interface")

return cmd
}

func dev(out io.Writer) error {
func dev(out io.Writer, ui bool) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
catchCtrlC(cancel)
if !ui {
catchCtrlC(cancel)
}

cleanup := func() {}
if opts.Cleanup {
Expand All @@ -60,6 +68,25 @@ func dev(out io.Writer) error {
}()
}

var (
app *tview.Application
output *config.Output
)
if ui {
app, output = createApp()
defer app.Stop()

go func() {
app.Run()
cancel()
}()
} else {
output = &config.Output{
Main: out,
Logs: out,
}
}

for {
select {
case <-ctx.Done():
Expand All @@ -70,7 +97,7 @@ func dev(out io.Writer) error {
return errors.Wrap(err, "creating runner")
}

err = r.Dev(ctx, out, config.Build.Artifacts)
err = r.Dev(ctx, output, config.Build.Artifacts)
if r.HasDeployed() {
cleanup = func() {
if err := r.Cleanup(context.Background(), out); err != nil {
Expand All @@ -86,3 +113,78 @@ func dev(out io.Writer) error {
}
}
}

func createApp() (*tview.Application, *config.Output) {
app := tview.NewApplication()

mainView := tview.NewTextView()
mainView.
SetChangedFunc(func() {
app.Draw()
}).
SetDynamicColors(true).
SetBorder(true).
SetTitle("Build")

logsView := tview.NewTextView()
logsView.
SetChangedFunc(func() {
app.Draw()
}).
SetDynamicColors(true).
SetBorder(true).
SetTitle("Logs")

grid := tview.NewGrid()
grid.
SetRows(0, 0).
SetColumns(0).
SetBorders(false).
AddItem(mainView, 0, 0, 1, 1, 0, 0, false).
AddItem(logsView, 1, 0, 1, 1, 0, 0, false)

app.
SetRoot(grid, true).
SetFocus(grid)

output := &config.Output{
Main: color.ColoredWriter{Writer: ansiWriter(mainView)},
Logs: color.ColoredWriter{Writer: ansiWriter(logsView)},
}

return app, output
}

func ansiWriter(writer io.Writer) io.Writer {
return &ansi{
Writer: writer,
replacer: strings.NewReplacer(
"\033[31m", "[maroon]",
"\033[32m", "[green]",
"\033[33m", "[olive]",
"\033[34m", "[navy]",
"\033[35m", "[purple]",
"\033[36m", "[teal]",
"\033[37m", "[silver]",

"\033[91m", "[red]",
"\033[92m", "[lime]",
"\033[93m", "[yellow]",
"\033[94m", "[blue]",
"\033[95m", "[fuchsia]",
"\033[96m", "[aqua]",
"\033[97m", "[white]",

"\033[0m", "",
),
}
}

type ansi struct {
io.Writer
replacer *strings.Replacer
}

func (a *ansi) Write(text []byte) (int, error) {
return a.replacer.WriteString(a.Writer, string(text))
}
2 changes: 2 additions & 0 deletions docs/content/en/docs/references/cli/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ Usage:
Flags:
--cleanup Delete deployments after dev mode is interrupted (default true)
-d, --default-repo string Default repository value (overrides global config)
--experimental-gui Experimental Graphical User Interface
-f, --filename string Filename or URL to the pipeline file (default "skaffold.yaml")
-l, --label stringArray Add custom labels to deployed objects. Set multiple times for multiple labels
-n, --namespace string Run deployments in the specified namespace
Expand All @@ -228,6 +229,7 @@ Env vars:

* `SKAFFOLD_CLEANUP` (same as --cleanup)
* `SKAFFOLD_DEFAULT_REPO` (same as --default-repo)
* `SKAFFOLD_EXPERIMENTAL_GUI` (same as --experimental-gui)
* `SKAFFOLD_FILENAME` (same as --filename)
* `SKAFFOLD_LABEL` (same as --label)
* `SKAFFOLD_NAMESPACE` (same as --namespace)
Expand Down
10 changes: 10 additions & 0 deletions pkg/skaffold/color/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ var (
Purple = Color(35)
// Cyan can format text to be displayed to the terminal in cyan, using ANSI escape codes.
Cyan = Color(36)
// White can format text to be displayed to the terminal in white, using ANSI escape codes.
White = Color(37)
// None uses ANSI escape codes to reset all formatting.
None = Color(0)

Expand Down Expand Up @@ -99,12 +101,20 @@ type ColoredWriteCloser struct {
io.WriteCloser
}

// ColoredWriter forces printing with colors to an io.Writer.
type ColoredWriter struct {
io.Writer
}

// This implementation comes from logrus (https://github.com/sirupsen/logrus/blob/master/terminal_check_notappengine.go),
// unfortunately logrus doesn't expose a public interface we can use to call it.
func isTerminal(w io.Writer) bool {
if _, ok := w.(ColoredWriteCloser); ok {
return true
}
if _, ok := w.(ColoredWriter); ok {
return true
}

switch v := w.(type) {
case *os.File:
Expand Down
8 changes: 8 additions & 0 deletions pkg/skaffold/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ limitations under the License.
package config

import (
"io"
"strings"
)

// Output defines which zones on the screen to print to
type Output struct {
Main io.Writer
Logs io.Writer
}

// SkaffoldOptions are options that are set by command line arguments not included
// in the config file itself
type SkaffoldOptions struct {
Expand All @@ -30,6 +37,7 @@ type SkaffoldOptions struct {
TailDev bool
PortForward bool
SkipTests bool
ExperimentalGUI bool
Profiles []string
CustomTag string
Namespace string
Expand Down
2 changes: 1 addition & 1 deletion pkg/skaffold/kubernetes/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func (a *LogAggregator) streamRequest(ctx context.Context, headerColor color.Col
if _, err := headerColor.Fprintf(a.output, "%s ", header); err != nil {
return errors.Wrap(err, "writing pod prefix header to out")
}
if _, err := fmt.Fprint(a.output, string(line)); err != nil {
if _, err := color.White.Fprint(a.output, string(line)); err != nil {
return errors.Wrap(err, "writing pod log to out")
}
}
Expand Down
18 changes: 9 additions & 9 deletions pkg/skaffold/runner/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ package runner

import (
"context"
"io"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/color"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync"
Expand All @@ -36,11 +36,11 @@ var ErrorConfigurationChanged = errors.New("configuration changed")

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

portForwarder := kubernetes.NewPortForwarder(out, r.imageList, r.namespaces)
portForwarder := kubernetes.NewPortForwarder(output.Main, r.imageList, r.namespaces)
defer portForwarder.Stop()

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

if err := r.Syncer.Sync(ctx, s); err != nil {
logrus.Warnln("Skipping deploy due to sync error:", err)
return nil
}
}
case len(changed.needsRebuild) > 0:
if err := r.buildTestDeploy(ctx, out, changed.needsRebuild); err != nil {
if err := r.buildTestDeploy(ctx, output.Main, changed.needsRebuild); err != nil {
logrus.Warnln("Skipping deploy due to error:", err)
return nil
}
case changed.needsRedeploy:
if err := r.Deploy(ctx, out, r.builds); err != nil {
if err := r.Deploy(ctx, output.Main, r.builds); err != nil {
logrus.Warnln("Skipping deploy due to error:", err)
return nil
}
Expand Down Expand Up @@ -131,7 +131,7 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
}

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

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

return r.Watcher.Run(ctx, out, onChange)
return r.Watcher.Run(ctx, output.Main, onChange)
}
14 changes: 11 additions & 3 deletions pkg/skaffold/runner/dev_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"io/ioutil"
"testing"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/watch"
"github.com/GoogleContainerTools/skaffold/testutil"
Expand Down Expand Up @@ -82,6 +83,13 @@ func (t *TestWatcher) Run(ctx context.Context, out io.Writer, onChange func() er
return nil
}

func discardOutput() *config.Output {
return &config.Output{
Main: ioutil.Discard,
Logs: ioutil.Discard,
}
}

func TestDevFailFirstCycle(t *testing.T) {
var tests = []struct {
description string
Expand Down Expand Up @@ -129,7 +137,7 @@ func TestDevFailFirstCycle(t *testing.T) {
runner := createRunner(t, test.testBench)
runner.Watcher = test.watcher

err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{{
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{{
ImageName: "img",
}})

Expand Down Expand Up @@ -260,7 +268,7 @@ func TestDev(t *testing.T) {
testBench: test.testBench,
}

err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{
{ImageName: "img1"},
{ImageName: "img2"},
})
Expand Down Expand Up @@ -325,7 +333,7 @@ func TestDevSync(t *testing.T) {
testBench: test.testBench,
}

err := runner.Dev(context.Background(), ioutil.Discard, []*latest.Artifact{
err := runner.Dev(context.Background(), discardOutput(), []*latest.Artifact{
{
ImageName: "img1",
Sync: map[string]string{
Expand Down

0 comments on commit 5a8bdcf

Please sign in to comment.