Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cli: add 'tilt alpha get' for fetching from the apiserver (#4401)
* 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
Showing
27 changed files
with
2,801 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.