Skip to content

Commit

Permalink
cli: add tilt alpha create cmd (#4440)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicks committed Apr 16, 2021
1 parent e941ca3 commit a56bdeb
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 50 deletions.
1 change: 0 additions & 1 deletion .python-version

This file was deleted.

1 change: 1 addition & 0 deletions internal/cli/create.go
Expand Up @@ -83,6 +83,7 @@ func (c *createCmd) register() *cobra.Command {
c.cmd = cmd

addCommand(cmd, newCreateFileWatchCmd())
addCommand(cmd, newCreateCmdCmd())

return cmd
}
Expand Down
147 changes: 147 additions & 0 deletions internal/cli/create_cmd.go
@@ -0,0 +1,147 @@
package cli

import (
"context"
"os"
"path/filepath"
"time"

"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/tilt-dev/tilt/internal/analytics"
engineanalytics "github.com/tilt-dev/tilt/internal/engine/analytics"
"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
"github.com/tilt-dev/tilt/pkg/model"
)

// A human-friendly CLI for creating cmds.
//
// The name is unfortunate.
//
// (as opposed to the machine-friendly CLIs of create -f or apply -f)
type createCmdCmd struct {
helper *createHelper

dir string
env []string
filewatches []string
}

var _ tiltCmd = &createCmdCmd{}

func newCreateCmdCmd() *createCmdCmd {
helper := newCreateHelper()
return &createCmdCmd{
helper: helper,
}
}

func (c *createCmdCmd) name() model.TiltSubcommand { return "cmd" }

func (c *createCmdCmd) register() *cobra.Command {
cmd := &cobra.Command{
Use: "cmd NAME COMMAND [ARG...]",
DisableFlagsInUseLine: true,
Short: "Create a local command in a running tilt session",
Long: `Create a local command in a running tilt session.
Intended to compose with other Tilt APIs that
can restart the command or monitor its status.
COMMAND should be an executable. A shell script will not work.
To run a shell script, use 'sh -c' (as shown in the examples).
`,
Args: cobra.MinimumNArgs(2),
Example: `
tilt alpha create cmd my-cmd echo hello world
tilt alpha create cmd my-cmd sh -c "echo hi && echo bye"
`,
}

// Interpret any flags after the object name as part of the
// command args.
cmd.Flags().SetInterspersed(false)

cmd.Flags().StringVarP(&c.dir, "workdir", "w", "",
"Working directory of the command. If not specified, uses the current working directory.")
cmd.Flags().StringArrayVarP(&c.env, "env", "e", nil,
"Set environment variables in the form NAME=VALUE.")
cmd.Flags().StringSliceVar(&c.filewatches, "filewatch", nil,
("Re-run the command whenever the named filewatches detect a change. " +
"See 'tilt create filewatch' for more."))

c.helper.addFlags(cmd)

return cmd
}

func (c *createCmdCmd) run(ctx context.Context, args []string) error {
a := analytics.Get(ctx)
cmdTags := engineanalytics.CmdTags(map[string]string{})
a.Incr("cmd.create-cmd", cmdTags.AsMap())
defer a.Flush(time.Second)

err := c.helper.interpretFlags(ctx)
if err != nil {
return err
}

cmd, err := c.object(args)
if err != nil {
return err
}

return c.helper.create(ctx, cmd)
}

// Interprets the flags specified on the commandline to the Cmd to create.
func (c *createCmdCmd) object(args []string) (*v1alpha1.Cmd, error) {
name := args[0]
command := args[1:]

dir, err := c.workdir()
if err != nil {
return nil, err
}

env := c.env
restartOn := c.restartOn()
cmd := v1alpha1.Cmd{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: v1alpha1.CmdSpec{
Args: command,
Dir: dir,
Env: env,
RestartOn: restartOn,
},
}
return &cmd, nil
}

// Determine the working directory of the command.
func (c *createCmdCmd) workdir() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}

if c.dir == "" {
return cwd, nil
}
if filepath.IsAbs(c.dir) {
return c.dir, nil
}
return filepath.Join(cwd, c.dir), nil
}

// Determine the restart conditions of the command.
func (c *createCmdCmd) restartOn() *v1alpha1.RestartOnSpec {
return &v1alpha1.RestartOnSpec{
FileWatches: c.filewatches,
}
}
42 changes: 42 additions & 0 deletions internal/cli/create_cmd_test.go
@@ -0,0 +1,42 @@
package cli

import (
"bytes"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/types"

"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
)

func TestCreateCmd(t *testing.T) {
f := newServerFixture(t)
defer f.TearDown()

out := bytes.NewBuffer(nil)

cmd := newCreateCmdCmd()
cmd.helper.streams.Out = out
c := cmd.register()
err := c.Flags().Parse([]string{
"--env", "COLOR=1",
"-e", "USER=nick",
})
require.NoError(t, err)

err = cmd.run(f.ctx, []string{"my-cmd", "echo", "hello", "world"})
require.NoError(t, err)
assert.Contains(t, out.String(), `cmd.tilt.dev/my-cmd created`)

var myCmd v1alpha1.Cmd
err = f.client.Get(f.ctx, types.NamespacedName{Name: "my-cmd"}, &myCmd)
require.NoError(t, err)

cwd, _ := os.Getwd()
assert.Equal(t, cwd, myCmd.Spec.Dir)
assert.Equal(t, []string{"echo", "hello", "world"}, myCmd.Spec.Args)
assert.Equal(t, []string{"COLOR=1", "USER=nick"}, myCmd.Spec.Env)
}
56 changes: 8 additions & 48 deletions internal/cli/create_filewatch.go
Expand Up @@ -8,10 +8,6 @@ import (

"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/dynamic"

"github.com/tilt-dev/tilt/internal/analytics"
engineanalytics "github.com/tilt-dev/tilt/internal/engine/analytics"
Expand All @@ -22,28 +18,23 @@ import (
// A human-friendly CLI for creating file watches.
//
// (as opposed to the machine-friendly CLIs of create -f or apply -f)
//
// TODO(nick): Refactor out the common parts of this, so that
// each human-friendly create CLI doesn't require all this boilerplate.
type createFileWatchCmd struct {
streams genericclioptions.IOStreams
printFlags *genericclioptions.PrintFlags
cmd *cobra.Command
helper *createHelper
cmd *cobra.Command

ignoreValues []string
}

var _ tiltCmd = &createFileWatchCmd{}

func newCreateFileWatchCmd() *createFileWatchCmd {
streams := genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr, In: os.Stdin}
helper := newCreateHelper()
return &createFileWatchCmd{
streams: streams,
printFlags: genericclioptions.NewPrintFlags("created"),
helper: helper,
}
}

func (c *createFileWatchCmd) name() model.TiltSubcommand { return "create-filewatch" }
func (c *createFileWatchCmd) name() model.TiltSubcommand { return "filewatch" }

func (c *createFileWatchCmd) register() *cobra.Command {
cmd := &cobra.Command{
Expand Down Expand Up @@ -71,8 +62,7 @@ trigger events when a file changes.
cmd.Flags().StringSliceVar(&c.ignoreValues, "ignore", nil,
"Patterns to ignore. Supports same syntax as .dockerignore. Paths are relative to the current directory.")

c.printFlags.AddFlags(cmd)
addConnectServerFlags(cmd)
c.helper.addFlags(cmd)
c.cmd = cmd

return cmd
Expand All @@ -84,12 +74,7 @@ func (c *createFileWatchCmd) run(ctx context.Context, args []string) error {
a.Incr("cmd.create-filewatch", cmdTags.AsMap())
defer a.Flush(time.Second)

printer, err := c.printFlags.ToPrinter()
if err != nil {
return err
}

dynamicClient, err := c.dynamicClient(ctx)
err := c.helper.interpretFlags(ctx)
if err != nil {
return err
}
Expand All @@ -99,32 +84,7 @@ func (c *createFileWatchCmd) run(ctx context.Context, args []string) error {
return err
}

obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fw)
if err != nil {
return err
}

result, err := dynamicClient.Resource(fw.GetGroupVersionResource()).
Create(ctx, &unstructured.Unstructured{Object: obj}, metav1.CreateOptions{})
if err != nil {
return err
}

return printer.PrintObj(result, c.streams.Out)
}

// Loads a dynamically typed tilt client.
func (c *createFileWatchCmd) dynamicClient(ctx context.Context) (dynamic.Interface, error) {
getter, err := wireClientGetter(ctx)
if err != nil {
return nil, err
}

config, err := getter.ToRESTConfig()
if err != nil {
return nil, err
}
return dynamic.NewForConfig(config)
return c.helper.create(ctx, fw)
}

// Interprets the flags specified on the commandline to the FileWatch to create.
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/create_filewatch_test.go
Expand Up @@ -20,7 +20,7 @@ func TestCreateFileWatch(t *testing.T) {
out := bytes.NewBuffer(nil)

cmd := newCreateFileWatchCmd()
cmd.streams.Out = out
cmd.helper.streams.Out = out
c := cmd.register()
err := c.Flags().Parse([]string{
"--ignore", "web/node_modules",
Expand Down
83 changes: 83 additions & 0 deletions internal/cli/create_helper.go
@@ -0,0 +1,83 @@
package cli

import (
"context"
"os"

"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/client-go/dynamic"

"github.com/tilt-dev/tilt-apiserver/pkg/server/builder/resource"
)

// Helper for human-friendly CLI for creating objects.
//
// See other create commands for usage examples.
type createHelper struct {
streams genericclioptions.IOStreams
printFlags *genericclioptions.PrintFlags
dynamicClient dynamic.Interface
printer printers.ResourcePrinter
}

func newCreateHelper() *createHelper {
streams := genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr, In: os.Stdin}
return &createHelper{
streams: streams,
printFlags: genericclioptions.NewPrintFlags("created"),
}
}

func (h *createHelper) addFlags(cmd *cobra.Command) {
h.printFlags.AddFlags(cmd)
addConnectServerFlags(cmd)
}

func (h *createHelper) interpretFlags(ctx context.Context) error {
printer, err := h.printFlags.ToPrinter()
if err != nil {
return err
}
h.printer = printer

dynamicClient, err := h.createDynamicClient(ctx)
if err != nil {
return err
}
h.dynamicClient = dynamicClient
return nil
}

func (h *createHelper) create(ctx context.Context, resourceObj resource.Object) error {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resourceObj)
if err != nil {
return err
}

result, err := h.dynamicClient.Resource(resourceObj.GetGroupVersionResource()).
Create(ctx, &unstructured.Unstructured{Object: obj}, metav1.CreateOptions{})
if err != nil {
return err
}

return h.printer.PrintObj(result, h.streams.Out)
}

// Loads a dynamically typed tilt client.
func (h *createHelper) createDynamicClient(ctx context.Context) (dynamic.Interface, error) {
getter, err := wireClientGetter(ctx)
if err != nil {
return nil, err
}

config, err := getter.ToRESTConfig()
if err != nil {
return nil, err
}
return dynamic.NewForConfig(config)
}

0 comments on commit a56bdeb

Please sign in to comment.