Skip to content

Commit

Permalink
[client-go] Polymorphic Scale Client
Browse files Browse the repository at this point in the history
This introduces a polymorphic scale client capable of operating against
scale subresources which return different group-versions of Scale.  The
scale subresources may be in group-versions different than the scale
itself, so that we no longer need a copy of every scalable resource in
the extensions API group.

To discovery which Scale group-versions go to which subresources,
discovery is used.

The scale client maintains its own internal versions and conversions to
several external versions, with a "hub" version that's a copy of the
autoscaling internal version.

It currently supports the following group-versions for Scale subresources:

- extensions/v1beta1.Scale
- autoscaling/v1.Scale
  • Loading branch information
DirectXMan12 committed Oct 19, 2017
1 parent f83a196 commit d61a2d9
Show file tree
Hide file tree
Showing 29 changed files with 1,780 additions and 2 deletions.
5 changes: 5 additions & 0 deletions hack/.golint_failures
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,11 @@ staging/src/k8s.io/client-go/plugin/pkg/auth/authenticator/token/oidc/testing
staging/src/k8s.io/client-go/plugin/pkg/client/auth/oidc
staging/src/k8s.io/client-go/rest
staging/src/k8s.io/client-go/rest/fake
staging/src/k8s.io/client-go/scale
staging/src/k8s.io/client-go/scale/scheme
staging/src/k8s.io/client-go/scale/scheme/autoscalingv1
staging/src/k8s.io/client-go/scale/scheme/extensionsint
staging/src/k8s.io/client-go/scale/scheme/extensionsv1beta1
staging/src/k8s.io/client-go/testing
staging/src/k8s.io/client-go/tools/cache
staging/src/k8s.io/client-go/tools/cache/testing
Expand Down
1 change: 1 addition & 0 deletions staging/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ filegroup(
"//staging/src/k8s.io/client-go/plugin/pkg/auth/authenticator/token/oidc/testing:all-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth:all-srcs",
"//staging/src/k8s.io/client-go/rest:all-srcs",
"//staging/src/k8s.io/client-go/scale:all-srcs",
"//staging/src/k8s.io/client-go/testing:all-srcs",
"//staging/src/k8s.io/client-go/third_party/forked/golang/template:all-srcs",
"//staging/src/k8s.io/client-go/tools/auth:all-srcs",
Expand Down
20 changes: 20 additions & 0 deletions staging/src/k8s.io/client-go/Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions staging/src/k8s.io/client-go/rest/url_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ func DefaultServerURL(host, apiPath string, groupVersion schema.GroupVersion, de
// hostURL.Path should be blank.
//
// versionedAPIPath, a path relative to baseURL.Path, points to a versioned API base
versionedAPIPath := DefaultVersionedAPIPath(apiPath, groupVersion)

return hostURL, versionedAPIPath, nil
}

// DefaultVersionedAPIPathFor constructs the default path for the given group version, assuming the given
// API path, following the standard conventions of the Kubernetes API.
func DefaultVersionedAPIPath(apiPath string, groupVersion schema.GroupVersion) string {
versionedAPIPath := path.Join("/", apiPath)

// Add the version to the end of the path
Expand All @@ -64,10 +72,9 @@ func DefaultServerURL(host, apiPath string, groupVersion schema.GroupVersion, de

} else {
versionedAPIPath = path.Join(versionedAPIPath, groupVersion.Version)

}

return hostURL, versionedAPIPath, nil
return versionedAPIPath
}

// defaultServerUrlFor is shared between IsConfigTransportTLS and RESTClientFor. It
Expand Down
72 changes: 72 additions & 0 deletions staging/src/k8s.io/client-go/scale/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = [
"client.go",
"doc.go",
"interfaces.go",
"util.go",
],
importpath = "k8s.io/client-go/scale",
visibility = ["//visibility:public"],
deps = [
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/scale/scheme:go_default_library",
"//vendor/k8s.io/client-go/scale/scheme/autoscalingv1:go_default_library",
"//vendor/k8s.io/client-go/scale/scheme/extensionsint:go_default_library",
"//vendor/k8s.io/client-go/scale/scheme/extensionsv1beta1:go_default_library",
],
)

go_test(
name = "go_default_test",
srcs = [
"client_test.go",
"roundtrip_test.go",
],
importpath = "k8s.io/client-go/scale",
library = ":go_default_library",
deps = [
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/api/apps/v1beta2:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/testing/roundtrip:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/discovery/fake:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/rest/fake:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
],
)

filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)

filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//staging/src/k8s.io/client-go/scale/scheme:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
197 changes: 197 additions & 0 deletions staging/src/k8s.io/client-go/scale/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
Copyright 2017 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 scale

import (
"fmt"

autoscaling "k8s.io/api/autoscaling/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/dynamic"
restclient "k8s.io/client-go/rest"
)

var scaleConverter = NewScaleConverter()
var codecs = serializer.NewCodecFactory(scaleConverter.Scheme())

// restInterfaceProvider turns a restclient.Config into a restclient.Interface.
// It's overridable for the purposes of testing.
type restInterfaceProvider func(*restclient.Config) (restclient.Interface, error)

// scaleClient is an implementation of ScalesGetter
// which makes use of a RESTMapper and a generic REST
// client to support an discoverable resource.
// It behaves somewhat similarly to the dynamic ClientPool,
// but is more specifically scoped to Scale.
type scaleClient struct {
mapper meta.RESTMapper

apiPathResolverFunc dynamic.APIPathResolverFunc
scaleKindResolver ScaleKindResolver
clientBase restclient.Interface
}

// NewForConfig creates a new ScalesGetter which resolves kinds
// to resources using the given RESTMapper, and API paths using
// the given dynamic.APIPathResolverFunc.
func NewForConfig(cfg *restclient.Config, mapper meta.RESTMapper, resolver dynamic.APIPathResolverFunc, scaleKindResolver ScaleKindResolver) (ScalesGetter, error) {
// so that the RESTClientFor doesn't complain
cfg.GroupVersion = &schema.GroupVersion{}

cfg.NegotiatedSerializer = serializer.DirectCodecFactory{
CodecFactory: codecs,
}
if len(cfg.UserAgent) == 0 {
cfg.UserAgent = restclient.DefaultKubernetesUserAgent()
}

client, err := restclient.RESTClientFor(cfg)
if err != nil {
return nil, err
}

return New(client, mapper, resolver, scaleKindResolver), nil
}

// New creates a new ScalesGetter using the given client to make requests.
// The GroupVersion on the client is ignored.
func New(baseClient restclient.Interface, mapper meta.RESTMapper, resolver dynamic.APIPathResolverFunc, scaleKindResolver ScaleKindResolver) ScalesGetter {
return &scaleClient{
mapper: mapper,

apiPathResolverFunc: resolver,
scaleKindResolver: scaleKindResolver,
clientBase: baseClient,
}
}

// pathAndVersionFor returns the appropriate base path and the associated full GroupVersionResource
// for the given GroupResource
func (c *scaleClient) pathAndVersionFor(resource schema.GroupResource) (string, schema.GroupVersionResource, error) {
gvr, err := c.mapper.ResourceFor(resource.WithVersion(""))
if err != nil {
return "", gvr, fmt.Errorf("unable to get full preferred group-version-resource for %s: %v", resource.String(), err)
}

groupVer := gvr.GroupVersion()

// we need to set the API path based on GroupVersion (defaulting to the legacy path if none is set)
// TODO: we "cheat" here since the API path really only depends on group ATM, but this should
// *probably* take GroupVersionResource and not GroupVersionKind.
apiPath := c.apiPathResolverFunc(groupVer.WithKind(""))
if apiPath == "" {
apiPath = "/api"
}

path := restclient.DefaultVersionedAPIPath(apiPath, groupVer)

return path, gvr, nil
}

// namespacedScaleClient is an ScaleInterface for fetching
// Scales in a given namespace.
type namespacedScaleClient struct {
client *scaleClient
namespace string
}

func (c *scaleClient) Scales(namespace string) ScaleInterface {
return &namespacedScaleClient{
client: c,
namespace: namespace,
}
}

func (c *namespacedScaleClient) Get(resource schema.GroupResource, name string) (*autoscaling.Scale, error) {
// Currently, a /scale endpoint can return different scale types.
// Until we have support for the alternative API representations proposal,
// we need to deal with accepting different API versions.
// In practice, this is autoscaling/v1.Scale and extensions/v1beta1.Scale

path, gvr, err := c.client.pathAndVersionFor(resource)
if err != nil {
return nil, fmt.Errorf("unable to get client for %s: %v", resource.String(), err)
}

rawObj, err := c.client.clientBase.Get().
AbsPath(path).
Namespace(c.namespace).
Resource(gvr.Resource).
Name(name).
SubResource("scale").
Do().
Get()

if err != nil {
return nil, err
}

// convert whatever this is to autoscaling/v1.Scale
scaleObj, err := scaleConverter.ConvertToVersion(rawObj, autoscaling.SchemeGroupVersion)
if err != nil {
return nil, fmt.Errorf("received an object from a /scale endpoint which was not convertible to autoscaling Scale: %v", err)
}

return scaleObj.(*autoscaling.Scale), nil
}

func (c *namespacedScaleClient) Update(resource schema.GroupResource, scale *autoscaling.Scale) (*autoscaling.Scale, error) {
path, gvr, err := c.client.pathAndVersionFor(resource)
if err != nil {
return nil, fmt.Errorf("unable to get client for %s: %v", resource.String(), err)
}

// Currently, a /scale endpoint can receive and return different scale types.
// Until we hvae support for the alternative API representations proposal,
// we need to deal with sending and accepting differnet API versions.

// figure out what scale we actually need here
desiredGVK, err := c.client.scaleKindResolver.ScaleForResource(gvr)
if err != nil {
return nil, fmt.Errorf("could not find proper group-version for scale subresource of %s: %v", gvr.String(), err)
}

// convert this to whatever this endpoint wants
scaleUpdate, err := scaleConverter.ConvertToVersion(scale, desiredGVK.GroupVersion())
if err != nil {
return nil, fmt.Errorf("could not convert scale update to internal Scale: %v", err)
}

rawObj, err := c.client.clientBase.Put().
AbsPath(path).
Namespace(c.namespace).
Resource(gvr.Resource).
Name(scale.Name).
SubResource("scale").
Body(scaleUpdate).
Do().
Get()

if err != nil {
return nil, fmt.Errorf("could not fetch the scale for %s %s: %v", resource.String(), scale.Name, err)
}

// convert whatever this is back to autoscaling/v1.Scale
scaleObj, err := scaleConverter.ConvertToVersion(rawObj, autoscaling.SchemeGroupVersion)
if err != nil {
return nil, fmt.Errorf("received an object from a /scale endpoint which was not convertible to autoscaling Scale: %v", err)
}

return scaleObj.(*autoscaling.Scale), err
}
Loading

0 comments on commit d61a2d9

Please sign in to comment.