diff --git a/cmd/kubectl/kubectl.go b/cmd/kubectl/kubectl.go index 2f96c67196..5098d47454 100644 --- a/cmd/kubectl/kubectl.go +++ b/cmd/kubectl/kubectl.go @@ -19,11 +19,15 @@ package main import ( "os" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" + + "github.com/golang/glog" ) func main() { - clientBuilder := clientcmd.NewInteractiveClientConfig(clientcmd.Config{}, "", &clientcmd.ConfigOverrides{}, os.Stdin) - cmd.NewFactory(clientBuilder).Run(os.Stdout) + cmd := cmd.NewFactory().NewKubectlCommand(os.Stdout) + if err := cmd.Execute(); err != nil { + glog.Errorf("error: %v", err) + os.Exit(1) + } } diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 71db26a8d8..f2fd6fb945 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -33,6 +33,7 @@ import ( "github.com/golang/glog" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) const ( @@ -42,55 +43,108 @@ const ( // Factory provides abstractions that allow the Kubectl command to be extended across multiple types // of resources and different API sets. type Factory struct { - ClientConfig clientcmd.ClientConfig - Mapper meta.RESTMapper - Typer runtime.ObjectTyper - Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) - Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) - Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) - Validator func(*cobra.Command) (validation.Schema, error) + clients *clientCache + flags *pflag.FlagSet + + Mapper meta.RESTMapper + Typer runtime.ObjectTyper + + // Returns a client for accessing Kubernetes resources or an error. + Client func(cmd *cobra.Command) (*client.Client, error) + // Returns a client.Config for accessing the Kubernetes server. + ClientConfig func(cmd *cobra.Command) (*client.Config, error) + // Returns a RESTClient for working with the specified RESTMapping or an error. This is intended + // for working with arbitrary resources and is not guaranteed to point to a Kubernetes APIServer. + RESTClient func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) + // Returns a Describer for displaying the specified RESTMapping type or an error. + Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) + // Returns a Printer for formatting objects of the given type or an error. + Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) + // Returns a schema that can validate objects stored on disk. + Validator func(*cobra.Command) (validation.Schema, error) } // NewFactory creates a factory with the default Kubernetes resources defined -func NewFactory(clientConfig clientcmd.ClientConfig) *Factory { - ret := &Factory{ - ClientConfig: clientConfig, - Mapper: latest.RESTMapper, - Typer: api.Scheme, - Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { - return kubectl.NewHumanReadablePrinter(noHeaders), nil - }, +func NewFactory() *Factory { + mapper := kubectl.ShortcutExpander{latest.RESTMapper} + + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + clientConfig := DefaultClientConfig(flags) + clients := &clientCache{ + clients: make(map[string]*client.Client), + loader: clientConfig, } - ret.Validator = func(cmd *cobra.Command) (validation.Schema, error) { - if GetFlagBool(cmd, "validate") { - client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) + return &Factory{ + clients: clients, + flags: flags, + + Mapper: mapper, + Typer: api.Scheme, + + Client: func(cmd *cobra.Command) (*client.Client, error) { + return clients.ClientForVersion("") + }, + ClientConfig: func(cmd *cobra.Command) (*client.Config, error) { + return clients.ClientConfigForVersion("") + }, + RESTClient: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { + client, err := clients.ClientForVersion(mapping.APIVersion) if err != nil { return nil, err } - return &clientSwaggerSchema{client, api.Scheme}, nil - } else { + return client.RESTClient, nil + }, + Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { + client, err := clients.ClientForVersion(mapping.APIVersion) + if err != nil { + return nil, err + } + describer, ok := kubectl.DescriberFor(mapping.Kind, client) + if !ok { + return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) + } + return describer, nil + }, + Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { + return kubectl.NewHumanReadablePrinter(noHeaders), nil + }, + Validator: func(cmd *cobra.Command) (validation.Schema, error) { + if GetFlagBool(cmd, "validate") { + client, err := clients.ClientForVersion("") + if err != nil { + return nil, err + } + return &clientSwaggerSchema{client, api.Scheme}, nil + } return validation.NullSchema{}, nil - } + }, } - ret.Client = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { - return getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) - } - ret.Describer = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { - client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) - if err != nil { - return nil, err - } - describer, ok := kubectl.DescriberFor(mapping.Kind, client) - if !ok { - return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) - } - return describer, nil - } - return ret } -func (f *Factory) Run(out io.Writer) { +// BindFlags adds any flags that are common to all kubectl sub commands. +func (f *Factory) BindFlags(flags *pflag.FlagSet) { + // any flags defined by external projects (not part of pflags) + util.AddAllFlagsToPFlagSet(flags) + + if f.flags != nil { + f.flags.VisitAll(func(flag *pflag.Flag) { + flags.AddFlag(flag) + }) + } + + // Globally persistent flags across all subcommands. + // TODO Change flag names to consts to allow safer lookup from subcommands. + // TODO Add a verbose flag that turns on glog logging. Probably need a way + // to do that automatically for every subcommand. + flags.BoolVar(&f.clients.matchVersion, FlagMatchBinaryVersion, false, "Require server version to match client version") + flags.String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.") + flags.StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.") + flags.Bool("validate", false, "If true, use a schema to validate the input before sending it") +} + +// NewKubectlCommand creates the `kubectl` command and its nested children. +func (f *Factory) NewKubectlCommand(out io.Writer) *cobra.Command { // Parent command to which all subcommands are added. cmds := &cobra.Command{ Use: "kubectl", @@ -101,15 +155,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, Run: runHelp, } - util.AddAllFlagsToPFlagSet(cmds.PersistentFlags()) - f.ClientConfig = getClientConfig(cmds) - - // Globally persistent flags across all subcommands. - // TODO Change flag names to consts to allow safer lookup from subcommands. - cmds.PersistentFlags().Bool(FlagMatchBinaryVersion, false, "Require server version to match client version") - cmds.PersistentFlags().String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.") - cmds.PersistentFlags().StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.") - cmds.PersistentFlags().Bool("validate", false, "If true, use a schema to validate the input before sending it") + f.BindFlags(cmds.PersistentFlags()) cmds.AddCommand(f.NewCmdVersion(out)) cmds.AddCommand(f.NewCmdProxy(out)) @@ -125,12 +171,10 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(f.NewCmdLog(out)) cmds.AddCommand(f.NewCmdRollingUpdate(out)) - if err := cmds.Execute(); err != nil { - os.Exit(1) - } + return cmds } -// getClientBuilder creates a clientcmd.ClientConfig that has a hierarchy like this: +// DefaultClientConfig creates a clientcmd.ClientConfig with the following hierarchy: // 1. Use the kubeconfig builder. The number of merges and overrides here gets a little crazy. Stay with me. // 1. Merge together the kubeconfig itself. This is done with the following hierarchy and merge rules: // 1. CommandLineLocation - this parsed from the command line, so it must be late bound @@ -162,13 +206,13 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, // 2. If the command line does not specify one, and the auth info has conflicting techniques, fail. // 3. If the command line specifies one and the auth info specifies another, honor the command line technique. // 2. Use default values and potentially prompt for auth information -func getClientConfig(cmd *cobra.Command) clientcmd.ClientConfig { +func DefaultClientConfig(flags *pflag.FlagSet) clientcmd.ClientConfig { loadingRules := clientcmd.NewClientConfigLoadingRules() loadingRules.EnvVarPath = os.Getenv(clientcmd.RecommendedConfigPathEnvVar) - cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + flags.StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") overrides := &clientcmd.ConfigOverrides{} - overrides.BindFlags(cmd.PersistentFlags(), clientcmd.RecommendedConfigOverrideFlags("")) + overrides.BindFlags(flags, clientcmd.RecommendedConfigOverrideFlags("")) clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin) return clientConfig @@ -246,18 +290,55 @@ func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { return schema.ValidateBytes(data) } -// TODO Need to only run server version match once per client host creation -func getClient(clientConfig clientcmd.ClientConfig, matchServerVersion bool) (*client.Client, error) { - config, err := clientConfig.ClientConfig() +// clientCache caches previously loaded clients for reuse, and ensures MatchServerVersion +// is invoked only once +type clientCache struct { + loader clientcmd.ClientConfig + clients map[string]*client.Client + defaultConfig *client.Config + matchVersion bool +} + +// ClientConfigForVersion returns the correct config for a server +func (c *clientCache) ClientConfigForVersion(version string) (*client.Config, error) { + if c.defaultConfig == nil { + config, err := c.loader.ClientConfig() + if err != nil { + return nil, err + } + c.defaultConfig = config + + if c.matchVersion { + if err := client.MatchesServerVersion(config); err != nil { + return nil, err + } + } + } + + // TODO: remove when SetKubernetesDefaults gets added + if len(version) == 0 { + version = c.defaultConfig.Version + } + + // TODO: have a better config copy method + config := *c.defaultConfig + + // TODO: call new client.SetKubernetesDefaults method + // instead of doing this + config.Version = version + return &config, nil +} + +// ClientForVersion initializes or reuses a client for the specified version, or returns an +// error if that is not possible +func (c *clientCache) ClientForVersion(version string) (*client.Client, error) { + config, err := c.ClientConfigForVersion(version) if err != nil { return nil, err } - if matchServerVersion { - err := client.MatchesServerVersion(config) - if err != nil { - return nil, err - } + if client, ok := c.clients[config.Version]; ok { + return client, nil } client, err := client.New(config) @@ -265,5 +346,6 @@ func getClient(clientConfig clientcmd.ClientConfig, matchServerVersion bool) (*c return nil, err } + c.clients[config.Version] = client return client, nil } diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 2bbb8f5199..2bc3db5c14 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -100,7 +100,7 @@ func NewTestFactory() (*Factory, *testFactory, runtime.Codec) { return &Factory{ Mapper: mapper, Typer: scheme, - Client: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { + RESTClient: func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) { return t.Client, t.Err }, Describer: func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) { diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 2e6138c4f6..6d5502550d 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -46,7 +46,7 @@ Examples: schema, err := f.Validator(cmd) checkErr(err) mapping, namespace, name, data := ResourceFromFile(cmd, filename, f.Typer, f.Mapper, schema) - client, err := f.Client(cmd, mapping) + client, err := f.RESTClient(cmd, mapping) checkErr(err) // use the default namespace if not specified, or check for conflict with the file's namespace diff --git a/pkg/kubectl/cmd/createall.go b/pkg/kubectl/cmd/createall.go index b1ea7bb2d9..694fdf9d54 100644 --- a/pkg/kubectl/cmd/createall.go +++ b/pkg/kubectl/cmd/createall.go @@ -79,7 +79,7 @@ Examples: `, Run: func(cmd *cobra.Command, args []string) { clientFunc := func(mapper *meta.RESTMapping) (config.RESTClientPoster, error) { - client, err := f.Client(cmd, mapper) + client, err := f.RESTClient(cmd, mapper) checkErr(err) return client, nil } diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index 39e9ea781d..a6e408a52e 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -59,7 +59,7 @@ Examples: checkErr(err) selector := GetFlagString(cmd, "selector") found := 0 - ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.Client, schema).Visit(func(r *resource.Info) error { + ResourcesFromArgsOrFile(cmd, args, filename, selector, f.Typer, f.Mapper, f.RESTClient, schema).Visit(func(r *resource.Info) error { found++ if err := resource.NewHelper(r.Client, r.Mapping).Delete(r.Namespace, r.Name); err != nil { return err diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 46079a9800..014a616576 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -56,7 +56,7 @@ Examples: labelSelector, err := labels.ParseSelector(selector) checkErr(err) - client, err := f.Client(cmd, mapping) + client, err := f.RESTClient(cmd, mapping) checkErr(err) outputFormat := GetFlagString(cmd, "output") diff --git a/pkg/kubectl/cmd/log.go b/pkg/kubectl/cmd/log.go index 4b4eeea031..1e88248e2f 100644 --- a/pkg/kubectl/cmd/log.go +++ b/pkg/kubectl/cmd/log.go @@ -21,8 +21,6 @@ import ( "strconv" "github.com/spf13/cobra" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" ) func (f *Factory) NewCmdLog(out io.Writer) *cobra.Command { @@ -46,9 +44,7 @@ Examples: } namespace := GetKubeNamespace(cmd) - config, err := f.ClientConfig.ClientConfig() - checkErr(err) - client, err := client.New(config) + client, err := f.Client(cmd) checkErr(err) podID := args[0] diff --git a/pkg/kubectl/cmd/proxy.go b/pkg/kubectl/cmd/proxy.go index 85f2e85d75..f99f2bcee3 100644 --- a/pkg/kubectl/cmd/proxy.go +++ b/pkg/kubectl/cmd/proxy.go @@ -33,7 +33,7 @@ func (f *Factory) NewCmdProxy(out io.Writer) *cobra.Command { port := GetFlagInt(cmd, "port") glog.Infof("Starting to serve on localhost:%d", port) - clientConfig, err := f.ClientConfig.ClientConfig() + clientConfig, err := f.ClientConfig(cmd) checkErr(err) server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), clientConfig, port) diff --git a/pkg/kubectl/cmd/resource.go b/pkg/kubectl/cmd/resource.go index 93da904cd8..afee120932 100644 --- a/pkg/kubectl/cmd/resource.go +++ b/pkg/kubectl/cmd/resource.go @@ -63,7 +63,7 @@ func ResourcesFromArgsOrFile( } types := SplitResourceArgument(args[0]) for _, arg := range types { - resourceName := kubectl.ExpandResourceShortcut(arg) + resourceName := arg if len(resourceName) == 0 { usageError(cmd, "Unknown resource %s", resourceName) } @@ -91,7 +91,7 @@ func ResourceFromArgsOrFile(cmd *cobra.Command, args []string, filename string, } if len(args) == 2 { - resource := kubectl.ExpandResourceShortcut(args[0]) + resource := args[0] namespace = GetKubeNamespace(cmd) name = args[1] if len(name) == 0 || len(resource) == 0 { @@ -129,7 +129,7 @@ func ResourceFromArgs(cmd *cobra.Command, args []string, mapper meta.RESTMapper) usageError(cmd, "Must provide resource and name command line params") } - resource := kubectl.ExpandResourceShortcut(args[0]) + resource := args[0] namespace = GetKubeNamespace(cmd) name = args[1] if len(name) == 0 || len(resource) == 0 { @@ -152,7 +152,7 @@ func ResourceOrTypeFromArgs(cmd *cobra.Command, args []string, mapper meta.RESTM usageError(cmd, "Must provide resource or a resource and name as command line params") } - resource := kubectl.ExpandResourceShortcut(args[0]) + resource := args[0] if len(resource) == 0 { usageError(cmd, "Must provide resource or a resource and name as command line params") } diff --git a/pkg/kubectl/cmd/rollingupdate.go b/pkg/kubectl/cmd/rollingupdate.go index 8d1e8b88b2..5a8ed9818a 100644 --- a/pkg/kubectl/cmd/rollingupdate.go +++ b/pkg/kubectl/cmd/rollingupdate.go @@ -21,7 +21,6 @@ import ( "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/spf13/cobra" ) @@ -69,9 +68,7 @@ $ cat frontend-v2.json | kubectl rollingupdate frontend-v1 -f - err = CompareNamespaceFromFile(cmd, namespace) checkErr(err) - config, err := f.ClientConfig.ClientConfig() - checkErr(err) - client, err := client.New(config) + client, err := f.Client(cmd) checkErr(err) obj, err := mapping.Codec.Decode(data) diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index 32eac2ab53..eb7d05b1bb 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -46,7 +46,7 @@ Examples: schema, err := f.Validator(cmd) checkErr(err) mapping, namespace, name, data := ResourceFromFile(cmd, filename, f.Typer, f.Mapper, schema) - client, err := f.Client(cmd, mapping) + client, err := f.RESTClient(cmd, mapping) checkErr(err) err = CompareNamespaceFromFile(cmd, namespace) diff --git a/pkg/kubectl/cmd/version.go b/pkg/kubectl/cmd/version.go index 9dbedc4b0d..5e434a58e1 100644 --- a/pkg/kubectl/cmd/version.go +++ b/pkg/kubectl/cmd/version.go @@ -21,7 +21,6 @@ import ( "github.com/spf13/cobra" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" ) @@ -32,14 +31,13 @@ func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { if GetFlagBool(cmd, "client") { kubectl.GetClientVersion(out) - } else { - config, err := f.ClientConfig.ClientConfig() - checkErr(err) - client, err := client.New(config) - checkErr(err) - - kubectl.GetVersion(out, client) + return } + + client, err := f.Client(cmd) + checkErr(err) + + kubectl.GetVersion(out, client) }, } cmd.Flags().BoolP("client", "c", false, "Client version only (no server required)") diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 4a5a1d9ac7..ea09542047 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -111,12 +112,23 @@ func makeImageList(spec *api.PodSpec) string { return strings.Join(listOfImages(spec), ",") } -// ExpandResourceShortcut will return the expanded version of resource +// ShortcutExpander is a RESTMapper that can be used for Kubernetes +// resources. +type ShortcutExpander struct { + meta.RESTMapper +} + +// VersionAndKindForResource implements meta.RESTMapper. It expands the resource first, then invokes the wrapped +// mapper. +func (e ShortcutExpander) VersionAndKindForResource(resource string) (defaultVersion, kind string, err error) { + resource = expandResourceShortcut(resource) + return e.RESTMapper.VersionAndKindForResource(resource) +} + +// expandResourceShortcut will return the expanded version of resource // (something that a pkg/api/meta.RESTMapper can understand), if it is // indeed a shortcut. Otherwise, will return resource unmodified. -// TODO: Combine with RESTMapper stuff to provide a general solution -// to this problem. -func ExpandResourceShortcut(resource string) string { +func expandResourceShortcut(resource string) string { shortForms := map[string]string{ "po": "pods", "rc": "replicationcontrollers",