Skip to content

Commit

Permalink
cli: add 'tilt alpha get' for fetching from the apiserver (#4401)
Browse files Browse the repository at this point in the history
* cli: add 'tilt alpha get' for fetching from the apiserver

This is a fork of kubectl get, simplified a bit and adapted to Tilt's
Command conventions.

I expect this will eventually graduate to `tilt get` (replacing `tilt dump engine`),
and we'll do a similar thing with kubectl create/delete/apply/api-resources

* test: add a test for tilt alpha get
  • Loading branch information
nicks committed Apr 6, 2021
1 parent c48c42a commit 5c47262
Show file tree
Hide file tree
Showing 27 changed files with 2,801 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -53,6 +53,7 @@ require (
github.com/moby/buildkit v0.7.1-0.20200925001807-2b6cccb9b3e9
github.com/modern-go/reflect2 v1.0.1
github.com/opencontainers/go-digest v1.0.0
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/browser v0.0.0-20170505125900-c90ca0c84f15
github.com/pkg/errors v0.9.1
github.com/rivo/tview v0.0.0-20180926100353-bc39bf8d245d
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -361,6 +361,7 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
Expand Down Expand Up @@ -934,6 +935,7 @@ github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pkg/browser v0.0.0-20170505125900-c90ca0c84f15 h1:mrI+6Ae64Wjt+uahGe5we/sPS1sXjvfT3YjtawAVgps=
Expand Down
1 change: 1 addition & 0 deletions internal/cli/alpha.go
Expand Up @@ -15,6 +15,7 @@ The APIs of these commands may change frequently.
addCommand(result, newTiltfileResultCmd())
addCommand(result, newKubeconfigPathCmd())
addCommand(result, newUpdogCmd())
addCommand(result, newUpdogGetCmd())

return result
}
132 changes: 132 additions & 0 deletions internal/cli/client/getter.go
@@ -0,0 +1,132 @@
/*
Adapted from
https://github.com/kubernetes/cli-runtime/tree/master/pkg/genericclioptions
*/

/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package client

import (
"fmt"
"path/filepath"
"regexp"
"strings"
"time"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/discovery"
diskcached "k8s.io/client-go/discovery/cached/disk"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/util/homedir"
)

var (
defaultCacheDir = filepath.Join(homedir.HomeDir(), ".tilt-dev", "cache")
)

// Composes the set of values necessary
// for obtaining a REST client config
type Getter struct {
Host string
Port int
}

func NewGetter(host string, port int) *Getter {
return &Getter{Host: host, Port: port}
}

var _ genericclioptions.RESTClientGetter = &Getter{}

// ToRESTConfig implements RESTClientGetter.
// Returns a REST client configuration based on a provided path
// to a .kubeconfig file, loading rules, and config flag overrides.
// Expects the AddFlags method to have been called.
func (f *Getter) ToRESTConfig() (*rest.Config, error) {
return f.ToRawKubeConfigLoader().ClientConfig()
}

// ToRawKubeConfigLoader binds config flag values to config overrides
// Returns an interactive clientConfig if the password flag is enabled,
// or a non-interactive clientConfig otherwise.
func (f *Getter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
const apiserver = "tilt-apiserver"
config := clientcmdapi.NewConfig()
config.Clusters[apiserver] = &clientcmdapi.Cluster{
Server: fmt.Sprintf("http://%s:%d", f.Host, f.Port),
InsecureSkipTLSVerify: true,
}
config.AuthInfos[apiserver] = &clientcmdapi.AuthInfo{
Username: "corgi",
Password: "charge!!!",
}
config.Contexts[apiserver] = &clientcmdapi.Context{
Cluster: apiserver,
AuthInfo: apiserver,
}
config.CurrentContext = apiserver

return clientcmd.NewNonInteractiveClientConfig(*config, apiserver, &clientcmd.ConfigOverrides{}, nil)
}

// ToDiscoveryClient implements RESTClientGetter.
// Expects the AddFlags method to have been called.
// Returns a CachedDiscoveryInterface using a computed RESTConfig.
func (f *Getter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := f.ToRESTConfig()
if err != nil {
return nil, err
}

// The more groups you have, the more discovery requests you need to make.
// given 25 groups (our groups + a few custom resources) with one-ish version each, discovery needs to make 50 requests
// double it just so we don't end up here again for a while. This config is only used for discovery.
config.Burst = 100

cacheDir := defaultCacheDir
httpCacheDir := filepath.Join(cacheDir, "http")
discoveryCacheDir := computeDiscoverCacheDir(filepath.Join(cacheDir, "discovery"), config.Host)

return diskcached.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, time.Duration(10*time.Minute))
}

func (f *Getter) ToRESTMapper() (meta.RESTMapper, error) {
discoveryClient, err := f.ToDiscoveryClient()
if err != nil {
return nil, err
}

mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
expander := restmapper.NewShortcutExpander(mapper, discoveryClient)
return expander, nil
}

// overlyCautiousIllegalFileCharacters matches characters that *might* not be supported. Windows is really restrictive, so this is really restrictive
var overlyCautiousIllegalFileCharacters = regexp.MustCompile(`[^(\w/\.)]`)

// computeDiscoverCacheDir takes the parentDir and the host and comes up with a "usually non-colliding" name.
func computeDiscoverCacheDir(parentDir, host string) string {
// strip the optional scheme from host if its there:
schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1)
// now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely. Even if we do collide the problem is short lived
safeHost := overlyCautiousIllegalFileCharacters.ReplaceAllString(schemelessHost, "_")
return filepath.Join(parentDir, safeHost)
}
90 changes: 90 additions & 0 deletions internal/cli/updog_get.go
@@ -0,0 +1,90 @@
/*
Adapted from
https://github.com/kubernetes/kubectl/tree/master/pkg/cmd/get
*/

/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cli

import (
"context"
"os"
"time"

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/kubectl/pkg/cmd/get"
cmdutil "k8s.io/kubectl/pkg/cmd/util"

"github.com/tilt-dev/tilt/internal/analytics"
cliclient "github.com/tilt-dev/tilt/internal/cli/client"
engineanalytics "github.com/tilt-dev/tilt/internal/engine/analytics"
"github.com/tilt-dev/tilt/pkg/model"
)

type updogGetCmd struct {
options *get.GetOptions
cmd *cobra.Command
}

var _ tiltCmd = &updogCmd{}

func newUpdogGetCmd() *updogGetCmd {
streams := genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr, In: os.Stdin}
o := get.NewGetOptions("tilt alpha", streams)
return &updogGetCmd{
options: o,
}
}

func (c *updogGetCmd) name() model.TiltSubcommand { return "updog-get" }

func (c *updogGetCmd) register() *cobra.Command {
cmd := &cobra.Command{
Use: "get TYPE [NAME | -l label]",
DisableFlagsInUseLine: true,
Short: "Display one or many resources",
}
c.cmd = cmd
o := c.options

o.PrintFlags.AddFlags(cmd)

cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "After listing/getting the requested object, watch for changes. Uninitialized objects are excluded if no object name is provided.")
cmd.Flags().BoolVar(&o.WatchOnly, "watch-only", o.WatchOnly, "Watch for changes to the requested object(s), without listing/getting first.")
cmd.Flags().BoolVar(&o.IgnoreNotFound, "ignore-not-found", o.IgnoreNotFound, "If the requested object does not exist the command will return exit code 0.")
cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
cmd.Flags().StringVar(&o.FieldSelector, "field-selector", o.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
addConnectServerFlags(cmd)
return cmd
}

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

o := c.options
f := cmdutil.NewFactory(cliclient.NewGetter(defaultWebHost, defaultWebPort))
cmd := c.cmd
cmdutil.CheckErr(o.Complete(f, cmd, args))
cmdutil.CheckErr(o.Validate(cmd))
cmdutil.CheckErr(o.Run(f, cmd, args))
return nil
}
92 changes: 92 additions & 0 deletions internal/cli/updog_get_test.go
@@ -0,0 +1,92 @@
package cli

import (
"bytes"
"context"
"testing"

"github.com/phayes/freeport"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/tilt-dev/tilt/internal/hud/server"
"github.com/tilt-dev/tilt/internal/store"
"github.com/tilt-dev/tilt/internal/testutils"
"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
"github.com/tilt-dev/tilt/pkg/assets"
"github.com/tilt-dev/tilt/pkg/model"
)

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

err := f.client.Create(f.ctx, &v1alpha1.Cmd{
ObjectMeta: metav1.ObjectMeta{Name: "my-sleep"},
Spec: v1alpha1.CmdSpec{
Args: []string{"sleep", "1"},
},
})
require.NoError(t, err)

out := bytes.NewBuffer(nil)
updogGet := newUpdogGetCmd()
updogGet.register()
updogGet.options.IOStreams.Out = out

err = updogGet.run(f.ctx, []string{"cmd", "my-sleep"})
require.NoError(t, err)

assert.Contains(t, out.String(), `NAME CREATED AT
my-sleep`)
}

type serverFixture struct {
t *testing.T
ctx context.Context
cancel func()
hudsc *server.HeadsUpServerController
client ctrlclient.Client

origPort int
}

func newServerFixture(t *testing.T) *serverFixture {
ctx, _, _ := testutils.CtxAndAnalyticsForTest()
ctx, cancel := context.WithCancel(ctx)
memconn := server.ProvideMemConn()
port, err := freeport.GetFreePort()
require.NoError(t, err)

cfg, err := server.ProvideTiltServerOptions(ctx, "localhost", model.WebPort(port), model.TiltBuild{}, memconn)
require.NoError(t, err)

hudsc := server.ProvideHeadsUpServerController(model.WebPort(port), cfg, &server.HeadsUpServer{}, assets.NewFakeServer(), model.WebURL{})
st := store.NewTestingStore()
require.NoError(t, hudsc.SetUp(ctx, st))

scheme := v1alpha1.NewScheme()

client, err := ctrlclient.New(cfg.GenericConfig.LoopbackClientConfig, ctrlclient.Options{Scheme: scheme})
require.NoError(t, err)

origPort := defaultWebPort
defaultWebPort = port

return &serverFixture{
t: t,
ctx: ctx,
cancel: cancel,
hudsc: hudsc,
client: client,
origPort: origPort,
}
}

func (f *serverFixture) TearDown() {
f.hudsc.TearDown(f.ctx)
f.cancel()
defaultWebPort = f.origPort
}
19 changes: 19 additions & 0 deletions vendor/github.com/fvbommel/sortorder/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions vendor/github.com/fvbommel/sortorder/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions vendor/github.com/fvbommel/sortorder/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions vendor/github.com/fvbommel/sortorder/doc.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5c47262

Please sign in to comment.