Merge pull request #12405 from uluyol/kubectlexp

Add experimental api support to kubectl
This commit is contained in:
CJ Cullen
2015-08-12 14:24:56 -07:00
17 changed files with 210 additions and 70 deletions

View File

@@ -17,12 +17,13 @@ limitations under the License.
package cmd
import (
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/api"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
)
@@ -40,7 +41,7 @@ func NewCmdApiVersions(f *cmdutil.Factory, out io.Writer) *cobra.Command {
return cmd
}
func RunApiVersions(f *cmdutil.Factory, out io.Writer) error {
func RunApiVersions(f *cmdutil.Factory, w io.Writer) error {
if len(os.Args) > 1 && os.Args[1] == "apiversions" {
printDeprecationWarning("api-versions", "apiversions")
}
@@ -50,6 +51,24 @@ func RunApiVersions(f *cmdutil.Factory, out io.Writer) error {
return err
}
kubectl.GetApiVersions(out, client)
apiVersions, err := client.ServerAPIVersions()
if err != nil {
fmt.Printf("Couldn't get available api versions from server: %v\n", err)
os.Exit(1)
}
var expAPIVersions *api.APIVersions
showExpVersions := false
expClient, err := f.ExperimentalClient()
if err == nil {
expAPIVersions, err = expClient.ServerAPIVersions()
showExpVersions = err == nil
}
fmt.Fprintf(w, "Available Server Api Versions: %#v\n", *apiVersions)
if showExpVersions {
fmt.Fprintf(w, "Available Server Experimental Api Versions: %#v\n", *expAPIVersions)
}
return nil
}

View File

@@ -83,7 +83,7 @@ func newExternalScheme() (*runtime.Scheme, meta.RESTMapper, runtime.Codec) {
codec := runtime.CodecFor(scheme, "unlikelyversion")
validVersion := testapi.Version()
mapper := meta.NewDefaultRESTMapper([]string{"unlikelyversion", validVersion}, func(version string) (*meta.VersionInterfaces, error) {
mapper := meta.NewDefaultRESTMapper("apitest", []string{"unlikelyversion", validVersion}, func(version string) (*meta.VersionInterfaces, error) {
return &meta.VersionInterfaces{
Codec: runtime.CodecFor(scheme, version),
ObjectConvertor: scheme,

View File

@@ -22,17 +22,17 @@ import (
"k8s.io/kubernetes/pkg/client/clientcmd"
)
func NewClientCache(loader clientcmd.ClientConfig) *clientCache {
return &clientCache{
func NewClientCache(loader clientcmd.ClientConfig) *ClientCache {
return &ClientCache{
clients: make(map[string]*client.Client),
configs: make(map[string]*client.Config),
loader: loader,
}
}
// clientCache caches previously loaded clients for reuse, and ensures MatchServerVersion
// ClientCache caches previously loaded clients for reuse, and ensures MatchServerVersion
// is invoked only once
type clientCache struct {
type ClientCache struct {
loader clientcmd.ClientConfig
clients map[string]*client.Client
configs map[string]*client.Config
@@ -42,7 +42,7 @@ type clientCache struct {
}
// ClientConfigForVersion returns the correct config for a server
func (c *clientCache) ClientConfigForVersion(version string) (*client.Config, error) {
func (c *ClientCache) ClientConfigForVersion(version string) (*client.Config, error) {
if c.defaultConfig == nil {
config, err := c.loader.ClientConfig()
if err != nil {
@@ -73,7 +73,7 @@ func (c *clientCache) ClientConfigForVersion(version string) (*client.Config, er
// 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) {
func (c *ClientCache) ClientForVersion(version string) (*client.Client, error) {
if client, ok := c.clients[version]; ok {
return client, nil
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
package util
import (
"errors"
"flag"
"fmt"
"io"
@@ -27,7 +28,6 @@ import (
"github.com/spf13/pflag"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/latest"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/registered"
"k8s.io/kubernetes/pkg/api/validation"
@@ -49,7 +49,7 @@ const (
// TODO: pass the various interfaces on the factory directly into the command constructors (so the
// commands are decoupled from the factory).
type Factory struct {
clients *clientCache
clients *ClientCache
flags *pflag.FlagSet
generators map[string]kubectl.Generator
@@ -57,6 +57,8 @@ type Factory struct {
Object func() (meta.RESTMapper, runtime.ObjectTyper)
// Returns a client for accessing Kubernetes resources or an error.
Client func() (*client.Client, error)
// Returns a client for accessing experimental Kubernetes resources or an error.
ExperimentalClient func() (*client.ExperimentalClient, error)
// Returns a client.Config for accessing the Kubernetes server.
ClientConfig func() (*client.Config, error)
// Returns a RESTClient for working with the specified RESTMapping or an error. This is intended
@@ -90,7 +92,7 @@ type Factory struct {
// if optionalClientConfig is nil, then flags will be bound to a new clientcmd.ClientConfig.
// if optionalClientConfig is not nil, then this factory will make use of it.
func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
mapper := kubectl.ShortcutExpander{RESTMapper: latest.RESTMapper}
mapper := kubectl.ShortcutExpander{RESTMapper: api.RESTMapper}
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
flags.SetNormalizeFunc(util.WarnWordSepNormalizeFunc) // Warn for "_" flags
@@ -109,6 +111,25 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
clients := NewClientCache(clientConfig)
// Initialize the experimental client (if possible). Failing here is non-fatal, errors
// will be returned when an experimental client is explicitly requested.
var experimentalClient *client.ExperimentalClient
cfg, experimentalClientErr := clientConfig.ClientConfig()
if experimentalClientErr == nil {
experimentalClient, experimentalClientErr = client.NewExperimental(cfg)
}
noClientErr := errors.New("could not get client")
getBothClients := func(group string, version string) (client *client.Client, expClient *client.ExperimentalClient, err error) {
err = noClientErr
switch group {
case "api":
client, err = clients.ClientForVersion(version)
case "experimental":
expClient, err = experimentalClient, experimentalClientErr
}
return
}
return &Factory{
clients: clients,
flags: flags,
@@ -124,26 +145,46 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
Client: func() (*client.Client, error) {
return clients.ClientForVersion("")
},
ExperimentalClient: func() (*client.ExperimentalClient, error) {
return experimentalClient, experimentalClientErr
},
ClientConfig: func() (*client.Config, error) {
return clients.ClientConfigForVersion("")
},
RESTClient: func(mapping *meta.RESTMapping) (resource.RESTClient, error) {
client, err := clients.ClientForVersion(mapping.APIVersion)
group, err := api.RESTMapper.GroupForResource(mapping.Resource)
if err != nil {
return nil, err
}
return client.RESTClient, nil
switch group {
case "api":
client, err := clients.ClientForVersion(mapping.APIVersion)
if err != nil {
return nil, err
}
return client.RESTClient, nil
case "experimental":
client, err := experimentalClient, experimentalClientErr
if err != nil {
return nil, err
}
return client.RESTClient, nil
}
return nil, fmt.Errorf("unable to get RESTClient for resource '%s'", mapping.Resource)
},
Describer: func(mapping *meta.RESTMapping) (kubectl.Describer, error) {
client, err := clients.ClientForVersion(mapping.APIVersion)
group, err := api.RESTMapper.GroupForResource(mapping.Resource)
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)
client, expClient, err := getBothClients(group, mapping.APIVersion)
if err != nil {
return nil, err
}
return describer, nil
if describer, ok := kubectl.DescriberFor(mapping.Kind, client, expClient); ok {
return describer, nil
}
return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind)
},
Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, wide bool, columnLabels []string) (kubectl.ResourcePrinter, error) {
return kubectl.NewHumanReadablePrinter(noHeaders, withNamespace, wide, columnLabels), nil
@@ -192,18 +233,26 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
return meta.NewAccessor().Labels(object)
},
Scaler: func(mapping *meta.RESTMapping) (kubectl.Scaler, error) {
client, err := clients.ClientForVersion(mapping.APIVersion)
group, err := api.RESTMapper.GroupForResource(mapping.Resource)
if err != nil {
return nil, err
}
client, _, err := getBothClients(group, mapping.APIVersion)
if err != nil {
return nil, err
}
return kubectl.ScalerFor(mapping.Kind, kubectl.NewScalerClient(client))
},
Reaper: func(mapping *meta.RESTMapping) (kubectl.Reaper, error) {
client, err := clients.ClientForVersion(mapping.APIVersion)
group, err := api.RESTMapper.GroupForResource(mapping.Resource)
if err != nil {
return nil, err
}
return kubectl.ReaperFor(mapping.Kind, client)
client, expClient, err := getBothClients(group, mapping.APIVersion)
if err != nil {
return nil, err
}
return kubectl.ReaperFor(mapping.Kind, client, expClient)
},
Validator: func() (validation.Schema, error) {
if flags.Lookup("validate").Value.String() == "true" {
@@ -211,7 +260,7 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
if err != nil {
return nil, err
}
return &clientSwaggerSchema{client, api.Scheme}, nil
return &clientSwaggerSchema{client, experimentalClient, api.Scheme}, nil
}
return validation.NullSchema{}, nil
},
@@ -274,20 +323,14 @@ func getServicePorts(spec api.ServiceSpec) []string {
}
type clientSwaggerSchema struct {
c *client.Client
t runtime.ObjectTyper
c *client.Client
ec *client.ExperimentalClient
t runtime.ObjectTyper
}
func (c *clientSwaggerSchema) ValidateBytes(data []byte) error {
version, _, err := runtime.UnstructuredJSONScheme.DataVersionAndKind(data)
if err != nil {
return err
}
if ok := registered.IsRegisteredAPIVersion(version); !ok {
return fmt.Errorf("API version %q isn't supported, only supports API versions %q", version, registered.RegisteredVersions)
}
schemaData, err := c.c.RESTClient.Get().
AbsPath("/swaggerapi/api", version).
func getSchemaAndValidate(c *client.RESTClient, data []byte, group, version string) error {
schemaData, err := c.Get().
AbsPath("/swaggerapi", group, version).
Do().
Raw()
if err != nil {
@@ -300,6 +343,28 @@ func (c *clientSwaggerSchema) ValidateBytes(data []byte) error {
return schema.ValidateBytes(data)
}
func (c *clientSwaggerSchema) ValidateBytes(data []byte) error {
version, _, err := runtime.UnstructuredJSONScheme.DataVersionAndKind(data)
if err != nil {
return err
}
if ok := registered.IsRegisteredAPIVersion(version); !ok {
return fmt.Errorf("API version %q isn't supported, only supports API versions %q", version, registered.RegisteredVersions)
}
// First try stable api, if we can't validate using that, try experimental.
// If experimental fails, return error from stable api.
// TODO: Figure out which group to try once multiple group support is merged
// instead of trying everything.
err = getSchemaAndValidate(c.c.RESTClient, data, "api", version)
if err != nil && c.ec != nil {
errExp := getSchemaAndValidate(c.ec.RESTClient, data, "experimental", version)
if errExp == nil {
return nil
}
}
return err
}
// 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 rules: