Make expandResourceShortcuts part of RESTMapper on client

pull/6/head
Clayton Coleman 2014-12-28 00:27:52 -05:00
parent a1ee782df5
commit 8a4f225941
14 changed files with 185 additions and 96 deletions

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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

View File

@ -79,7 +79,7 @@ Examples:
<creates all resources listed in config.json>`,
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
}

View File

@ -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

View File

@ -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")

View File

@ -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]

View File

@ -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)

View File

@ -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")
}

View File

@ -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)

View File

@ -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)

View File

@ -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)")

View File

@ -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",