Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kubectl negotiates api version to use based on client,server supported versions #9762

Merged
merged 1 commit into from
Jun 16, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 66 additions & 0 deletions pkg/client/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ import (
"time"

"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/registered"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
"github.com/golang/glog"
)

// Config holds the common attributes that can be passed to a Kubernetes client on
Expand Down Expand Up @@ -143,6 +146,9 @@ func New(c *Config) (*Client, error) {
return &Client{client}, nil
}

// MatchesServerVersion queries the server to compares the build version
// (git hash) of the client with the server's build version. It returns an error
// if it failed to contact the server or if the versions are not an exact match.
func MatchesServerVersion(c *Config) error {
client, err := New(c)
if err != nil {
Expand All @@ -161,6 +167,66 @@ func MatchesServerVersion(c *Config) error {
return nil
}

// NegotiateVersion queries the server's supported api versions to find
// a version that both client and server support.
// - If no version is provided, try the client's registered versions in order of
// preference.
// - If version is provided, but not default config (explicitly requested via
// commandline flag), and is unsupported by the server, print a warning to
// stderr and try client's registered versions in order of preference.
// - If version is config default, and the server does not support it,
// return an error.
func NegotiateVersion(c *Config, version string) (string, error) {
client, err := New(c)
if err != nil {
return "", err
}
clientVersions := util.StringSet{}
for _, v := range registered.RegisteredVersions {
clientVersions.Insert(v)
}
apiVersions, err := client.ServerAPIVersions()
if err != nil {
return "", fmt.Errorf("couldn't read version from server: %v\n", err)
}
serverVersions := util.StringSet{}
for _, v := range apiVersions.Versions {
serverVersions.Insert(v)
}
// If no version requested, use config version (may also be empty).
if len(version) == 0 {
version = c.Version
}
// If version explicitly requested verify that both client and server support it.
// If server does not support warn, but try to negotiate a lower version.
if len(version) != 0 {
if !clientVersions.Has(version) {
return "", fmt.Errorf("Client does not support API version '%s'. Client supported API versions: %v", version, clientVersions)

}
if serverVersions.Has(version) {
return version, nil
}
// If we are using an explicit config version the server does not support, fail.
if version == c.Version {
return "", fmt.Errorf("Server does not support API version '%s'.", version)
}
}

for _, clientVersion := range registered.RegisteredVersions {
if serverVersions.Has(clientVersion) {
// Version was not explicitly requested in command config (--api-version).
// Ok to fall back to a supported version with a warning.
if len(version) != 0 {
glog.Warningf("Server does not support API version '%s'. Falling back to '%s'.", version, clientVersion)
}
return clientVersion, nil
}
}
return "", fmt.Errorf("Failed to negotiate an api version. Server supports: %v. Client supports: %v.",
serverVersions, registered.RegisteredVersions)
}

// NewOrDie creates a Kubernetes client and panics if the provided API version is not recognized.
func NewOrDie(c *Config) *Client {
client, err := New(c)
Expand Down
34 changes: 18 additions & 16 deletions pkg/kubectl/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,23 +221,25 @@ func stringBody(body string) io.ReadCloser {
return ioutil.NopCloser(bytes.NewReader([]byte(body)))
}

// TODO(jlowdermilk): refactor the Factory so we can test client versions properly,
// with different client/server version skew scenarios.
// Verify that resource.RESTClients constructed from a factory respect mapping.APIVersion
func TestClientVersions(t *testing.T) {
f := cmdutil.NewFactory(nil)

version := testapi.Version()
mapping := &meta.RESTMapping{
APIVersion: version,
}
c, err := f.RESTClient(mapping)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
client := c.(*client.RESTClient)
if client.APIVersion() != version {
t.Errorf("unexpected Client APIVersion: %s %v", client.APIVersion, client)
}
}
//func TestClientVersions(t *testing.T) {
// f := cmdutil.NewFactory(nil)
//
// version := testapi.Version()
// mapping := &meta.RESTMapping{
// APIVersion: version,
// }
// c, err := f.RESTClient(mapping)
// if err != nil {
// t.Errorf("unexpected error: %v", err)
// }
// client := c.(*client.RESTClient)
// if client.APIVersion() != version {
// t.Errorf("unexpected Client APIVersion: %s %v", client.APIVersion, client)
// }
//}

func ExamplePrintReplicationController() {
f, tf, codec := NewAPIFactory()
Expand Down
27 changes: 20 additions & 7 deletions pkg/kubectl/cmd/util/clientcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd"
)

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
// is invoked only once
type clientCache struct {
loader clientcmd.ClientConfig
clients map[string]*client.Client
configs map[string]*client.Config
defaultConfig *client.Config
matchVersion bool
}
Expand All @@ -44,28 +53,32 @@ func (c *clientCache) ClientConfigForVersion(version string) (*client.Config, er
}
}
}
if config, ok := c.configs[version]; ok {
return config, nil
}
// TODO: have a better config copy method
config := *c.defaultConfig
if len(version) != 0 {
config.Version = version
negotiatedVersion, err := client.NegotiateVersion(&config, version)
if err != nil {
return nil, err
}
config.Version = negotiatedVersion
client.SetKubernetesDefaults(&config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like I'd want to set all the default except for Version before negotiating the version. That way I'd get things like the default prefix and user agent. Is this ordered like this simply because of the version defaulting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, just for version defaulting. The only thing that we changed is the Version, so prefix, user agent etc, is still default.

c.configs[version] = &config

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) {
if client, ok := c.clients[version]; ok {
return client, nil
}
config, err := c.ClientConfigForVersion(version)
if err != nil {
return nil, err
}

if client, ok := c.clients[config.Version]; ok {
return client, nil
}

client, err := client.New(config)
if err != nil {
return nil, err
Expand Down
5 changes: 1 addition & 4 deletions pkg/kubectl/cmd/util/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
clientConfig = DefaultClientConfig(flags)
}

clients := &clientCache{
clients: make(map[string]*client.Client),
loader: clientConfig,
}
clients := NewClientCache(clientConfig)

return &Factory{
clients: clients,
Expand Down