mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Merge pull request #35418 from sttts/sttts-discovery-cache
Automatic merge from submit-queue
Add caching for discovery info with invalidation on cache-miss
TODO:
- [x] write tests for `CachedDiscoveryClient`
- [x] write tests for `DeferredDiscoveryRESTMapper` on cache-miss
- [x] find better way/structure to get rid of `invalidateCh` in c06ba3175b
This commit is contained in:
commit
f7e0c6c19e
@ -32,6 +32,7 @@ go_library(
|
|||||||
"//pkg/version:go_default_library",
|
"//pkg/version:go_default_library",
|
||||||
"//plugin/pkg/client/auth:go_default_library",
|
"//plugin/pkg/client/auth:go_default_library",
|
||||||
"//vendor:github.com/emicklei/go-restful/swagger",
|
"//vendor:github.com/emicklei/go-restful/swagger",
|
||||||
|
"//vendor:github.com/golang/glog",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -44,11 +45,15 @@ go_test(
|
|||||||
library = "go_default_library",
|
library = "go_default_library",
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//pkg/api/errors:go_default_library",
|
||||||
"//pkg/api/unversioned:go_default_library",
|
"//pkg/api/unversioned:go_default_library",
|
||||||
"//pkg/api/v1:go_default_library",
|
"//pkg/api/v1:go_default_library",
|
||||||
|
"//pkg/apimachinery/registered:go_default_library",
|
||||||
"//pkg/client/restclient:go_default_library",
|
"//pkg/client/restclient:go_default_library",
|
||||||
|
"//pkg/client/restclient/fake:go_default_library",
|
||||||
"//pkg/version:go_default_library",
|
"//pkg/version:go_default_library",
|
||||||
"//vendor:github.com/emicklei/go-restful/swagger",
|
"//vendor:github.com/emicklei/go-restful/swagger",
|
||||||
|
"//vendor:github.com/stretchr/testify/assert",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,6 +45,15 @@ type DiscoveryInterface interface {
|
|||||||
SwaggerSchemaInterface
|
SwaggerSchemaInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CachedDiscoveryInterface is a DiscoveryInterface with cache invalidation and freshness.
|
||||||
|
type CachedDiscoveryInterface interface {
|
||||||
|
DiscoveryInterface
|
||||||
|
// Fresh returns true if no cached data was used that had been retrieved before the instantiation.
|
||||||
|
Fresh() bool
|
||||||
|
// Invalidate enforces that no cached data is used in the future that is older than the current time.
|
||||||
|
Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
// ServerGroupsInterface has methods for obtaining supported groups on the API server
|
// ServerGroupsInterface has methods for obtaining supported groups on the API server
|
||||||
type ServerGroupsInterface interface {
|
type ServerGroupsInterface interface {
|
||||||
// ServerGroups returns the supported groups, with information like supported versions and the
|
// ServerGroups returns the supported groups, with information like supported versions and the
|
||||||
|
@ -23,6 +23,8 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/api/errors"
|
"k8s.io/kubernetes/pkg/api/errors"
|
||||||
"k8s.io/kubernetes/pkg/api/meta"
|
"k8s.io/kubernetes/pkg/api/meta"
|
||||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIGroupResources is an API group with a mapping of versions to
|
// APIGroupResources is an API group with a mapping of versions to
|
||||||
@ -35,7 +37,7 @@ type APIGroupResources struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRESTMapper returns a PriorityRESTMapper based on the discovered
|
// NewRESTMapper returns a PriorityRESTMapper based on the discovered
|
||||||
// groups and resourced passed in.
|
// groups and resources passed in.
|
||||||
func NewRESTMapper(groupResources []*APIGroupResources, versionInterfaces meta.VersionInterfacesFunc) meta.RESTMapper {
|
func NewRESTMapper(groupResources []*APIGroupResources, versionInterfaces meta.VersionInterfacesFunc) meta.RESTMapper {
|
||||||
unionMapper := meta.MultiRESTMapper{}
|
unionMapper := meta.MultiRESTMapper{}
|
||||||
|
|
||||||
@ -47,8 +49,8 @@ func NewRESTMapper(groupResources []*APIGroupResources, versionInterfaces meta.V
|
|||||||
groupPriority = append(groupPriority, group.Group.Name)
|
groupPriority = append(groupPriority, group.Group.Name)
|
||||||
|
|
||||||
if len(group.Group.PreferredVersion.Version) != 0 {
|
if len(group.Group.PreferredVersion.Version) != 0 {
|
||||||
preffered := group.Group.PreferredVersion.Version
|
preferred := group.Group.PreferredVersion.Version
|
||||||
if _, ok := group.VersionedResources[preffered]; ok {
|
if _, ok := group.VersionedResources[preferred]; ok {
|
||||||
resourcePriority = append(resourcePriority, unversioned.GroupVersionResource{
|
resourcePriority = append(resourcePriority, unversioned.GroupVersionResource{
|
||||||
Group: group.Group.Name,
|
Group: group.Group.Name,
|
||||||
Version: group.Group.PreferredVersion.Version,
|
Version: group.Group.PreferredVersion.Version,
|
||||||
@ -141,14 +143,14 @@ func GetAPIGroupResources(cl DiscoveryInterface) ([]*APIGroupResources, error) {
|
|||||||
type DeferredDiscoveryRESTMapper struct {
|
type DeferredDiscoveryRESTMapper struct {
|
||||||
initMu sync.Mutex
|
initMu sync.Mutex
|
||||||
delegate meta.RESTMapper
|
delegate meta.RESTMapper
|
||||||
cl DiscoveryInterface
|
cl CachedDiscoveryInterface
|
||||||
versionInterface meta.VersionInterfacesFunc
|
versionInterface meta.VersionInterfacesFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeferredDiscoveryRESTMapper returns a
|
// NewDeferredDiscoveryRESTMapper returns a
|
||||||
// DeferredDiscoveryRESTMapper that will lazily query the provided
|
// DeferredDiscoveryRESTMapper that will lazily query the provided
|
||||||
// client for discovery information to do REST mappings.
|
// client for discovery information to do REST mappings.
|
||||||
func NewDeferredDiscoveryRESTMapper(cl DiscoveryInterface, versionInterface meta.VersionInterfacesFunc) *DeferredDiscoveryRESTMapper {
|
func NewDeferredDiscoveryRESTMapper(cl CachedDiscoveryInterface, versionInterface meta.VersionInterfacesFunc) *DeferredDiscoveryRESTMapper {
|
||||||
return &DeferredDiscoveryRESTMapper{
|
return &DeferredDiscoveryRESTMapper{
|
||||||
cl: cl,
|
cl: cl,
|
||||||
versionInterface: versionInterface,
|
versionInterface: versionInterface,
|
||||||
@ -175,79 +177,118 @@ func (d *DeferredDiscoveryRESTMapper) getDelegate() (meta.RESTMapper, error) {
|
|||||||
// Reset resets the internally cached Discovery information and will
|
// Reset resets the internally cached Discovery information and will
|
||||||
// cause the next mapping request to re-discover.
|
// cause the next mapping request to re-discover.
|
||||||
func (d *DeferredDiscoveryRESTMapper) Reset() {
|
func (d *DeferredDiscoveryRESTMapper) Reset() {
|
||||||
|
glog.V(5).Info("Invalidating discovery information")
|
||||||
|
|
||||||
d.initMu.Lock()
|
d.initMu.Lock()
|
||||||
|
defer d.initMu.Unlock()
|
||||||
|
|
||||||
|
d.cl.Invalidate()
|
||||||
d.delegate = nil
|
d.delegate = nil
|
||||||
d.initMu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KindFor takes a partial resource and returns back the single match.
|
// KindFor takes a partial resource and returns back the single match.
|
||||||
// It returns an error if there are multiple matches.
|
// It returns an error if there are multiple matches.
|
||||||
func (d *DeferredDiscoveryRESTMapper) KindFor(resource unversioned.GroupVersionResource) (unversioned.GroupVersionKind, error) {
|
func (d *DeferredDiscoveryRESTMapper) KindFor(resource unversioned.GroupVersionResource) (gvk unversioned.GroupVersionKind, err error) {
|
||||||
del, err := d.getDelegate()
|
del, err := d.getDelegate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return unversioned.GroupVersionKind{}, err
|
return unversioned.GroupVersionKind{}, err
|
||||||
}
|
}
|
||||||
return del.KindFor(resource)
|
gvk, err = del.KindFor(resource)
|
||||||
|
if err != nil && !d.cl.Fresh() {
|
||||||
|
d.Reset()
|
||||||
|
gvk, err = d.KindFor(resource)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// KindsFor takes a partial resource and returns back the list of
|
// KindsFor takes a partial resource and returns back the list of
|
||||||
// potential kinds in priority order.
|
// potential kinds in priority order.
|
||||||
func (d *DeferredDiscoveryRESTMapper) KindsFor(resource unversioned.GroupVersionResource) ([]unversioned.GroupVersionKind, error) {
|
func (d *DeferredDiscoveryRESTMapper) KindsFor(resource unversioned.GroupVersionResource) (gvks []unversioned.GroupVersionKind, err error) {
|
||||||
del, err := d.getDelegate()
|
del, err := d.getDelegate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return del.KindsFor(resource)
|
gvks, err = del.KindsFor(resource)
|
||||||
|
if len(gvks) == 0 && !d.cl.Fresh() {
|
||||||
|
d.Reset()
|
||||||
|
gvks, err = d.KindsFor(resource)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceFor takes a partial resource and returns back the single
|
// ResourceFor takes a partial resource and returns back the single
|
||||||
// match. It returns an error if there are multiple matches.
|
// match. It returns an error if there are multiple matches.
|
||||||
func (d *DeferredDiscoveryRESTMapper) ResourceFor(input unversioned.GroupVersionResource) (unversioned.GroupVersionResource, error) {
|
func (d *DeferredDiscoveryRESTMapper) ResourceFor(input unversioned.GroupVersionResource) (gvr unversioned.GroupVersionResource, err error) {
|
||||||
del, err := d.getDelegate()
|
del, err := d.getDelegate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return unversioned.GroupVersionResource{}, err
|
return unversioned.GroupVersionResource{}, err
|
||||||
}
|
}
|
||||||
return del.ResourceFor(input)
|
gvr, err = del.ResourceFor(input)
|
||||||
|
if err != nil && !d.cl.Fresh() {
|
||||||
|
d.Reset()
|
||||||
|
gvr, err = d.ResourceFor(input)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourcesFor takes a partial resource and returns back the list of
|
// ResourcesFor takes a partial resource and returns back the list of
|
||||||
// potential resource in priority order.
|
// potential resource in priority order.
|
||||||
func (d *DeferredDiscoveryRESTMapper) ResourcesFor(input unversioned.GroupVersionResource) ([]unversioned.GroupVersionResource, error) {
|
func (d *DeferredDiscoveryRESTMapper) ResourcesFor(input unversioned.GroupVersionResource) (gvrs []unversioned.GroupVersionResource, err error) {
|
||||||
del, err := d.getDelegate()
|
del, err := d.getDelegate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return del.ResourcesFor(input)
|
gvrs, err = del.ResourcesFor(input)
|
||||||
|
if len(gvrs) == 0 && !d.cl.Fresh() {
|
||||||
|
d.Reset()
|
||||||
|
gvrs, err = d.ResourcesFor(input)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESTMapping identifies a preferred resource mapping for the
|
// RESTMapping identifies a preferred resource mapping for the
|
||||||
// provided group kind.
|
// provided group kind.
|
||||||
func (d *DeferredDiscoveryRESTMapper) RESTMapping(gk unversioned.GroupKind, versions ...string) (*meta.RESTMapping, error) {
|
func (d *DeferredDiscoveryRESTMapper) RESTMapping(gk unversioned.GroupKind, versions ...string) (m *meta.RESTMapping, err error) {
|
||||||
del, err := d.getDelegate()
|
del, err := d.getDelegate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return del.RESTMapping(gk, versions...)
|
m, err = del.RESTMapping(gk, versions...)
|
||||||
|
if err != nil && !d.cl.Fresh() {
|
||||||
|
d.Reset()
|
||||||
|
m, err = d.RESTMapping(gk, versions...)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESTMappings returns the RESTMappings for the provided group kind
|
// RESTMappings returns the RESTMappings for the provided group kind
|
||||||
// in a rough internal preferred order. If no kind is found, it will
|
// in a rough internal preferred order. If no kind is found, it will
|
||||||
// return a NoResourceMatchError.
|
// return a NoResourceMatchError.
|
||||||
func (d *DeferredDiscoveryRESTMapper) RESTMappings(gk unversioned.GroupKind) ([]*meta.RESTMapping, error) {
|
func (d *DeferredDiscoveryRESTMapper) RESTMappings(gk unversioned.GroupKind) (ms []*meta.RESTMapping, err error) {
|
||||||
del, err := d.getDelegate()
|
del, err := d.getDelegate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return del.RESTMappings(gk)
|
ms, err = del.RESTMappings(gk)
|
||||||
|
if len(ms) == 0 && !d.cl.Fresh() {
|
||||||
|
d.Reset()
|
||||||
|
ms, err = d.RESTMappings(gk)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// AliasesForResource returns whether a resource has an alias or not.
|
// AliasesForResource returns whether a resource has an alias or not.
|
||||||
func (d *DeferredDiscoveryRESTMapper) AliasesForResource(resource string) ([]string, bool) {
|
func (d *DeferredDiscoveryRESTMapper) AliasesForResource(resource string) (as []string, ok bool) {
|
||||||
del, err := d.getDelegate()
|
del, err := d.getDelegate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
return del.AliasesForResource(resource)
|
as, ok = del.AliasesForResource(resource)
|
||||||
|
if len(as) == 0 && !d.cl.Fresh() {
|
||||||
|
d.Reset()
|
||||||
|
as, ok = d.AliasesForResource(resource)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceSingularizer converts a resource name from plural to
|
// ResourceSingularizer converts a resource name from plural to
|
||||||
@ -257,7 +298,12 @@ func (d *DeferredDiscoveryRESTMapper) ResourceSingularizer(resource string) (sin
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resource, err
|
return resource, err
|
||||||
}
|
}
|
||||||
return del.ResourceSingularizer(resource)
|
singular, err = del.ResourceSingularizer(resource)
|
||||||
|
if err != nil && !d.cl.Fresh() {
|
||||||
|
d.Reset()
|
||||||
|
singular, err = d.ResourceSingularizer(resource)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DeferredDiscoveryRESTMapper) String() string {
|
func (d *DeferredDiscoveryRESTMapper) String() string {
|
||||||
|
@ -20,7 +20,15 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/api/errors"
|
||||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
|
"k8s.io/kubernetes/pkg/apimachinery/registered"
|
||||||
|
"k8s.io/kubernetes/pkg/client/restclient"
|
||||||
|
"k8s.io/kubernetes/pkg/client/restclient/fake"
|
||||||
|
"k8s.io/kubernetes/pkg/version"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful/swagger"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRESTMapper(t *testing.T) {
|
func TestRESTMapper(t *testing.T) {
|
||||||
@ -174,3 +182,144 @@ func TestRESTMapper(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeferredDiscoveryRESTMapper_CacheMiss(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cdc := fakeCachedDiscoveryInterface{fresh: false}
|
||||||
|
m := NewDeferredDiscoveryRESTMapper(&cdc, registered.InterfacesFor)
|
||||||
|
assert.False(cdc.fresh, "should NOT be fresh after instantiation")
|
||||||
|
assert.Zero(cdc.invalidateCalls, "should not have called Invalidate()")
|
||||||
|
|
||||||
|
gvk, err := m.KindFor(unversioned.GroupVersionResource{
|
||||||
|
Group: "a",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "foo",
|
||||||
|
})
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.True(cdc.fresh, "should be fresh after a cache-miss")
|
||||||
|
assert.Equal(cdc.invalidateCalls, 1, "should have called Invalidate() once")
|
||||||
|
assert.Equal(gvk.Kind, "Foo")
|
||||||
|
|
||||||
|
gvk, err = m.KindFor(unversioned.GroupVersionResource{
|
||||||
|
Group: "a",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "foo",
|
||||||
|
})
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(cdc.invalidateCalls, 1, "should NOT have called Invalidate() again")
|
||||||
|
|
||||||
|
gvk, err = m.KindFor(unversioned.GroupVersionResource{
|
||||||
|
Group: "a",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "bar",
|
||||||
|
})
|
||||||
|
assert.Error(err)
|
||||||
|
assert.Equal(cdc.invalidateCalls, 1, "should NOT have called Invalidate() again after another cache-miss, but with fresh==true")
|
||||||
|
|
||||||
|
cdc.fresh = false
|
||||||
|
gvk, err = m.KindFor(unversioned.GroupVersionResource{
|
||||||
|
Group: "a",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "bar",
|
||||||
|
})
|
||||||
|
assert.Error(err)
|
||||||
|
assert.Equal(cdc.invalidateCalls, 2, "should HAVE called Invalidate() again after another cache-miss, but with fresh==false")
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeCachedDiscoveryInterface struct {
|
||||||
|
invalidateCalls int
|
||||||
|
fresh bool
|
||||||
|
enabledA bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ CachedDiscoveryInterface = &fakeCachedDiscoveryInterface{}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) Fresh() bool {
|
||||||
|
return c.fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) Invalidate() {
|
||||||
|
c.invalidateCalls = c.invalidateCalls + 1
|
||||||
|
c.fresh = true
|
||||||
|
c.enabledA = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) RESTClient() restclient.Interface {
|
||||||
|
return &fake.RESTClient{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) ServerGroups() (*unversioned.APIGroupList, error) {
|
||||||
|
if c.enabledA {
|
||||||
|
return &unversioned.APIGroupList{
|
||||||
|
Groups: []unversioned.APIGroup{
|
||||||
|
{
|
||||||
|
Name: "a",
|
||||||
|
Versions: []unversioned.GroupVersionForDiscovery{
|
||||||
|
{
|
||||||
|
GroupVersion: "a/v1",
|
||||||
|
Version: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreferredVersion: unversioned.GroupVersionForDiscovery{
|
||||||
|
GroupVersion: "a/v1",
|
||||||
|
Version: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return &unversioned.APIGroupList{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) ServerResourcesForGroupVersion(groupVersion string) (*unversioned.APIResourceList, error) {
|
||||||
|
if c.enabledA && groupVersion == "a/v1" {
|
||||||
|
return &unversioned.APIResourceList{
|
||||||
|
GroupVersion: "a/v1",
|
||||||
|
APIResources: []unversioned.APIResource{
|
||||||
|
{
|
||||||
|
Name: "foo",
|
||||||
|
Kind: "Foo",
|
||||||
|
Namespaced: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.NewNotFound(unversioned.GroupResource{}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) ServerResources() (map[string]*unversioned.APIResourceList, error) {
|
||||||
|
if c.enabledA {
|
||||||
|
av1, _ := c.ServerResourcesForGroupVersion("a/v1")
|
||||||
|
return map[string]*unversioned.APIResourceList{
|
||||||
|
"a/v1": av1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return map[string]*unversioned.APIResourceList{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) ServerPreferredResources() ([]unversioned.GroupVersionResource, error) {
|
||||||
|
if c.enabledA {
|
||||||
|
return []unversioned.GroupVersionResource{
|
||||||
|
{
|
||||||
|
Group: "a",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "foo",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return []unversioned.GroupVersionResource{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) ServerPreferredNamespacedResources() ([]unversioned.GroupVersionResource, error) {
|
||||||
|
return []unversioned.GroupVersionResource{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) ServerVersion() (*version.Info, error) {
|
||||||
|
return &version.Info{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCachedDiscoveryInterface) SwaggerSchema(version unversioned.GroupVersion) (*swagger.ApiDeclaration, error) {
|
||||||
|
return &swagger.ApiDeclaration{}, nil
|
||||||
|
}
|
||||||
|
@ -133,6 +133,17 @@ func newExternalScheme() (*runtime.Scheme, meta.RESTMapper, runtime.Codec) {
|
|||||||
return scheme, mapper, codec
|
return scheme, mapper, codec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeCachedDiscoveryClient struct {
|
||||||
|
discovery.DiscoveryInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *fakeCachedDiscoveryClient) Fresh() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *fakeCachedDiscoveryClient) Invalidate() {
|
||||||
|
}
|
||||||
|
|
||||||
type TestFactory struct {
|
type TestFactory struct {
|
||||||
Mapper meta.RESTMapper
|
Mapper meta.RESTMapper
|
||||||
Typer runtime.ObjectTyper
|
Typer runtime.ObjectTyper
|
||||||
@ -164,6 +175,14 @@ func NewTestFactory() (cmdutil.Factory, *TestFactory, runtime.Codec, runtime.Neg
|
|||||||
}, t, codec, negotiatedSerializer
|
}, t, codec, negotiatedSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeFactory) DiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||||
|
discoveryClient, err := discovery.NewDiscoveryClientForConfig(f.tf.ClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &fakeCachedDiscoveryClient{DiscoveryInterface: discoveryClient}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FakeFactory) FlagSet() *pflag.FlagSet {
|
func (f *FakeFactory) FlagSet() *pflag.FlagSet {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ load(
|
|||||||
go_library(
|
go_library(
|
||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"cached_discovery.go",
|
||||||
"clientcache.go",
|
"clientcache.go",
|
||||||
"factory.go",
|
"factory.go",
|
||||||
"helpers.go",
|
"helpers.go",
|
||||||
@ -50,8 +51,10 @@ go_library(
|
|||||||
"//pkg/util/errors:go_default_library",
|
"//pkg/util/errors:go_default_library",
|
||||||
"//pkg/util/exec:go_default_library",
|
"//pkg/util/exec:go_default_library",
|
||||||
"//pkg/util/flag:go_default_library",
|
"//pkg/util/flag:go_default_library",
|
||||||
|
"//pkg/util/homedir:go_default_library",
|
||||||
"//pkg/util/sets:go_default_library",
|
"//pkg/util/sets:go_default_library",
|
||||||
"//pkg/util/strategicpatch:go_default_library",
|
"//pkg/util/strategicpatch:go_default_library",
|
||||||
|
"//pkg/version:go_default_library",
|
||||||
"//pkg/watch:go_default_library",
|
"//pkg/watch:go_default_library",
|
||||||
"//vendor:github.com/emicklei/go-restful/swagger",
|
"//vendor:github.com/emicklei/go-restful/swagger",
|
||||||
"//vendor:github.com/evanphx/json-patch",
|
"//vendor:github.com/evanphx/json-patch",
|
||||||
@ -64,6 +67,7 @@ go_library(
|
|||||||
go_test(
|
go_test(
|
||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"cached_discovery_test.go",
|
||||||
"factory_test.go",
|
"factory_test.go",
|
||||||
"helpers_test.go",
|
"helpers_test.go",
|
||||||
"shortcut_restmapper_test.go",
|
"shortcut_restmapper_test.go",
|
||||||
@ -85,8 +89,10 @@ go_test(
|
|||||||
"//pkg/apimachinery/registered:go_default_library",
|
"//pkg/apimachinery/registered:go_default_library",
|
||||||
"//pkg/apis/extensions:go_default_library",
|
"//pkg/apis/extensions:go_default_library",
|
||||||
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
|
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
|
||||||
|
"//pkg/client/restclient:go_default_library",
|
||||||
"//pkg/client/restclient/fake:go_default_library",
|
"//pkg/client/restclient/fake:go_default_library",
|
||||||
"//pkg/client/testing/core:go_default_library",
|
"//pkg/client/testing/core:go_default_library",
|
||||||
|
"//pkg/client/typed/discovery:go_default_library",
|
||||||
"//pkg/client/unversioned/clientcmd:go_default_library",
|
"//pkg/client/unversioned/clientcmd:go_default_library",
|
||||||
"//pkg/client/unversioned/clientcmd/api:go_default_library",
|
"//pkg/client/unversioned/clientcmd/api:go_default_library",
|
||||||
"//pkg/controller:go_default_library",
|
"//pkg/controller:go_default_library",
|
||||||
@ -97,6 +103,9 @@ go_test(
|
|||||||
"//pkg/util/exec:go_default_library",
|
"//pkg/util/exec:go_default_library",
|
||||||
"//pkg/util/flag:go_default_library",
|
"//pkg/util/flag:go_default_library",
|
||||||
"//pkg/util/validation/field:go_default_library",
|
"//pkg/util/validation/field:go_default_library",
|
||||||
|
"//pkg/version:go_default_library",
|
||||||
"//pkg/watch:go_default_library",
|
"//pkg/watch:go_default_library",
|
||||||
|
"//vendor:github.com/emicklei/go-restful/swagger",
|
||||||
|
"//vendor:github.com/stretchr/testify/assert",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
252
pkg/kubectl/cmd/util/cached_discovery.go
Normal file
252
pkg/kubectl/cmd/util/cached_discovery.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful/swagger"
|
||||||
|
"github.com/golang/glog"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
|
"k8s.io/kubernetes/pkg/client/restclient"
|
||||||
|
"k8s.io/kubernetes/pkg/client/typed/discovery"
|
||||||
|
"k8s.io/kubernetes/pkg/runtime"
|
||||||
|
"k8s.io/kubernetes/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CachedDiscoveryClient implements the functions that discovery server-supported API groups,
|
||||||
|
// versions and resources.
|
||||||
|
type CachedDiscoveryClient struct {
|
||||||
|
delegate discovery.DiscoveryInterface
|
||||||
|
|
||||||
|
// cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well.
|
||||||
|
cacheDirectory string
|
||||||
|
|
||||||
|
// ttl is how long the cache should be considered valid
|
||||||
|
ttl time.Duration
|
||||||
|
|
||||||
|
// mutex protects the variables below
|
||||||
|
mutex sync.Mutex
|
||||||
|
|
||||||
|
// ourFiles are all filenames of cache files created by this process
|
||||||
|
ourFiles map[string]struct{}
|
||||||
|
// invalidated is true if all cache files should be ignored that are not ours (e.g. after Invalidate() was called)
|
||||||
|
invalidated bool
|
||||||
|
// fresh is true if all used cache files were ours
|
||||||
|
fresh bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ discovery.CachedDiscoveryInterface = &CachedDiscoveryClient{}
|
||||||
|
|
||||||
|
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
|
||||||
|
func (d *CachedDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*unversioned.APIResourceList, error) {
|
||||||
|
filename := filepath.Join(d.cacheDirectory, groupVersion, "serverresources.json")
|
||||||
|
cachedBytes, err := d.getCachedFile(filename)
|
||||||
|
// don't fail on errors, we either don't have a file or won't be able to run the cached check. Either way we can fallback.
|
||||||
|
if err == nil {
|
||||||
|
cachedResources := &unversioned.APIResourceList{}
|
||||||
|
if err := runtime.DecodeInto(api.Codecs.UniversalDecoder(), cachedBytes, cachedResources); err == nil {
|
||||||
|
glog.V(6).Infof("returning cached discovery info from %v", filename)
|
||||||
|
return cachedResources, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
liveResources, err := d.delegate.ServerResourcesForGroupVersion(groupVersion)
|
||||||
|
if err != nil {
|
||||||
|
return liveResources, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.writeCachedFile(filename, liveResources); err != nil {
|
||||||
|
glog.V(3).Infof("failed to write cache to %v due to %v", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return liveResources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerResources returns the supported resources for all groups and versions.
|
||||||
|
func (d *CachedDiscoveryClient) ServerResources() (map[string]*unversioned.APIResourceList, error) {
|
||||||
|
apiGroups, err := d.ServerGroups()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groupVersions := unversioned.ExtractGroupVersions(apiGroups)
|
||||||
|
result := map[string]*unversioned.APIResourceList{}
|
||||||
|
for _, groupVersion := range groupVersions {
|
||||||
|
resources, err := d.ServerResourcesForGroupVersion(groupVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result[groupVersion] = resources
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) ServerGroups() (*unversioned.APIGroupList, error) {
|
||||||
|
filename := filepath.Join(d.cacheDirectory, "servergroups.json")
|
||||||
|
cachedBytes, err := d.getCachedFile(filename)
|
||||||
|
// don't fail on errors, we either don't have a file or won't be able to run the cached check. Either way we can fallback.
|
||||||
|
if err == nil {
|
||||||
|
cachedGroups := &unversioned.APIGroupList{}
|
||||||
|
if err := runtime.DecodeInto(api.Codecs.UniversalDecoder(), cachedBytes, cachedGroups); err == nil {
|
||||||
|
glog.V(6).Infof("returning cached discovery info from %v", filename)
|
||||||
|
return cachedGroups, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
liveGroups, err := d.delegate.ServerGroups()
|
||||||
|
if err != nil {
|
||||||
|
return liveGroups, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.writeCachedFile(filename, liveGroups); err != nil {
|
||||||
|
glog.V(3).Infof("failed to write cache to %v due to %v", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return liveGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) getCachedFile(filename string) ([]byte, error) {
|
||||||
|
// after invalidation ignore cache files not created by this process
|
||||||
|
d.mutex.Lock()
|
||||||
|
_, ourFile := d.ourFiles[filename]
|
||||||
|
if d.invalidated && !ourFile {
|
||||||
|
d.mutex.Unlock()
|
||||||
|
return nil, errors.New("cache invalidated")
|
||||||
|
}
|
||||||
|
d.mutex.Unlock()
|
||||||
|
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(fileInfo.ModTime().Add(d.ttl)) {
|
||||||
|
return nil, errors.New("cache expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the cache is present and its valid. Try to read and use it.
|
||||||
|
cachedBytes, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
d.fresh = d.fresh && ourFile
|
||||||
|
|
||||||
|
return cachedBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) writeCachedFile(filename string, obj runtime.Object) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := runtime.Encode(api.Codecs.LegacyCodec(), obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
_, err = f.Write(bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.Chmod(0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := f.Name()
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// atomic rename
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
err = os.Rename(name, filename)
|
||||||
|
if err == nil {
|
||||||
|
d.ourFiles[filename] = struct{}{}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) RESTClient() restclient.Interface {
|
||||||
|
return d.delegate.RESTClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) ServerPreferredResources() ([]unversioned.GroupVersionResource, error) {
|
||||||
|
return d.delegate.ServerPreferredResources()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) ServerPreferredNamespacedResources() ([]unversioned.GroupVersionResource, error) {
|
||||||
|
return d.delegate.ServerPreferredNamespacedResources()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) ServerVersion() (*version.Info, error) {
|
||||||
|
return d.delegate.ServerVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) SwaggerSchema(version unversioned.GroupVersion) (*swagger.ApiDeclaration, error) {
|
||||||
|
return d.delegate.SwaggerSchema(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) Fresh() bool {
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
|
||||||
|
return d.fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CachedDiscoveryClient) Invalidate() {
|
||||||
|
d.mutex.Lock()
|
||||||
|
defer d.mutex.Unlock()
|
||||||
|
|
||||||
|
d.ourFiles = map[string]struct{}{}
|
||||||
|
d.fresh = true
|
||||||
|
d.invalidated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCachedDiscoveryClient creates a new DiscoveryClient. cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well.
|
||||||
|
func NewCachedDiscoveryClient(delegate discovery.DiscoveryInterface, cacheDirectory string, ttl time.Duration) *CachedDiscoveryClient {
|
||||||
|
return &CachedDiscoveryClient{
|
||||||
|
delegate: delegate,
|
||||||
|
cacheDirectory: cacheDirectory,
|
||||||
|
ttl: ttl,
|
||||||
|
ourFiles: map[string]struct{}{},
|
||||||
|
fresh: true,
|
||||||
|
}
|
||||||
|
}
|
164
pkg/kubectl/cmd/util/cached_discovery_test.go
Normal file
164
pkg/kubectl/cmd/util/cached_discovery_test.go
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 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 util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful/swagger"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/api/errors"
|
||||||
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
|
"k8s.io/kubernetes/pkg/client/restclient"
|
||||||
|
"k8s.io/kubernetes/pkg/client/restclient/fake"
|
||||||
|
"k8s.io/kubernetes/pkg/client/typed/discovery"
|
||||||
|
"k8s.io/kubernetes/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCachedDiscoveryClient_Fresh(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
d, err := ioutil.TempDir("", "")
|
||||||
|
assert.NoError(err)
|
||||||
|
defer os.RemoveAll(d)
|
||||||
|
|
||||||
|
c := fakeDiscoveryClient{}
|
||||||
|
cdc := NewCachedDiscoveryClient(&c, d, 60*time.Second)
|
||||||
|
assert.True(cdc.Fresh(), "should be fresh after creation")
|
||||||
|
|
||||||
|
cdc.ServerGroups()
|
||||||
|
assert.True(cdc.Fresh(), "should be fresh after groups call without cache")
|
||||||
|
assert.Equal(c.groupCalls, 1)
|
||||||
|
|
||||||
|
cdc.ServerGroups()
|
||||||
|
assert.True(cdc.Fresh(), "should be fresh after another groups call")
|
||||||
|
assert.Equal(c.groupCalls, 1)
|
||||||
|
|
||||||
|
cdc.ServerResources()
|
||||||
|
assert.True(cdc.Fresh(), "should be fresh after resources call")
|
||||||
|
assert.Equal(c.resourceCalls, 1)
|
||||||
|
|
||||||
|
cdc.ServerResources()
|
||||||
|
assert.True(cdc.Fresh(), "should be fresh after another resources call")
|
||||||
|
assert.Equal(c.resourceCalls, 1)
|
||||||
|
|
||||||
|
cdc = NewCachedDiscoveryClient(&c, d, 60*time.Second)
|
||||||
|
cdc.ServerGroups()
|
||||||
|
assert.False(cdc.Fresh(), "should NOT be fresh after recreation with existing groups cache")
|
||||||
|
assert.Equal(c.groupCalls, 1)
|
||||||
|
|
||||||
|
cdc.ServerResources()
|
||||||
|
assert.False(cdc.Fresh(), "should NOT be fresh after recreation with existing resources cache")
|
||||||
|
assert.Equal(c.resourceCalls, 1)
|
||||||
|
|
||||||
|
cdc.Invalidate()
|
||||||
|
assert.True(cdc.Fresh(), "should be fresh after cache invalidation")
|
||||||
|
|
||||||
|
cdc.ServerResources()
|
||||||
|
assert.True(cdc.Fresh(), "should ignore existing resources cache after invalidation")
|
||||||
|
assert.Equal(c.resourceCalls, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCachedDiscoveryClient_TTL(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
d, err := ioutil.TempDir("", "")
|
||||||
|
assert.NoError(err)
|
||||||
|
defer os.RemoveAll(d)
|
||||||
|
|
||||||
|
c := fakeDiscoveryClient{}
|
||||||
|
cdc := NewCachedDiscoveryClient(&c, d, 1*time.Nanosecond)
|
||||||
|
cdc.ServerGroups()
|
||||||
|
assert.Equal(c.groupCalls, 1)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
cdc.ServerGroups()
|
||||||
|
assert.Equal(c.groupCalls, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeDiscoveryClient struct {
|
||||||
|
groupCalls int
|
||||||
|
resourceCalls int
|
||||||
|
versionCalls int
|
||||||
|
swaggerCalls int
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ discovery.DiscoveryInterface = &fakeDiscoveryClient{}
|
||||||
|
|
||||||
|
func (c *fakeDiscoveryClient) RESTClient() restclient.Interface {
|
||||||
|
return &fake.RESTClient{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeDiscoveryClient) ServerGroups() (*unversioned.APIGroupList, error) {
|
||||||
|
c.groupCalls = c.groupCalls + 1
|
||||||
|
return &unversioned.APIGroupList{
|
||||||
|
Groups: []unversioned.APIGroup{
|
||||||
|
{
|
||||||
|
Name: "a",
|
||||||
|
Versions: []unversioned.GroupVersionForDiscovery{
|
||||||
|
{
|
||||||
|
GroupVersion: "a/v1",
|
||||||
|
Version: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PreferredVersion: unversioned.GroupVersionForDiscovery{
|
||||||
|
GroupVersion: "a/v1",
|
||||||
|
Version: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*unversioned.APIResourceList, error) {
|
||||||
|
c.resourceCalls = c.resourceCalls + 1
|
||||||
|
if groupVersion == "a/v1" {
|
||||||
|
return &unversioned.APIResourceList{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.NewNotFound(unversioned.GroupResource{}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeDiscoveryClient) ServerResources() (map[string]*unversioned.APIResourceList, error) {
|
||||||
|
c.resourceCalls = c.resourceCalls + 1
|
||||||
|
return map[string]*unversioned.APIResourceList{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeDiscoveryClient) ServerPreferredResources() ([]unversioned.GroupVersionResource, error) {
|
||||||
|
c.resourceCalls = c.resourceCalls + 1
|
||||||
|
return []unversioned.GroupVersionResource{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeDiscoveryClient) ServerPreferredNamespacedResources() ([]unversioned.GroupVersionResource, error) {
|
||||||
|
c.resourceCalls = c.resourceCalls + 1
|
||||||
|
return []unversioned.GroupVersionResource{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeDiscoveryClient) ServerVersion() (*version.Info, error) {
|
||||||
|
c.versionCalls = c.versionCalls + 1
|
||||||
|
return &version.Info{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeDiscoveryClient) SwaggerSchema(version unversioned.GroupVersion) (*swagger.ApiDeclaration, error) {
|
||||||
|
c.swaggerCalls = c.swaggerCalls + 1
|
||||||
|
return &swagger.ApiDeclaration{}, nil
|
||||||
|
}
|
@ -27,6 +27,7 @@ import (
|
|||||||
"os/user"
|
"os/user"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -61,6 +62,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/runtime"
|
"k8s.io/kubernetes/pkg/runtime"
|
||||||
"k8s.io/kubernetes/pkg/runtime/serializer/json"
|
"k8s.io/kubernetes/pkg/runtime/serializer/json"
|
||||||
utilflag "k8s.io/kubernetes/pkg/util/flag"
|
utilflag "k8s.io/kubernetes/pkg/util/flag"
|
||||||
|
"k8s.io/kubernetes/pkg/util/homedir"
|
||||||
"k8s.io/kubernetes/pkg/watch"
|
"k8s.io/kubernetes/pkg/watch"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -77,6 +79,8 @@ type Factory interface {
|
|||||||
// Returns internal flagset
|
// Returns internal flagset
|
||||||
FlagSet() *pflag.FlagSet
|
FlagSet() *pflag.FlagSet
|
||||||
|
|
||||||
|
// Returns a discovery client
|
||||||
|
DiscoveryClient() (discovery.CachedDiscoveryInterface, error)
|
||||||
// Returns interfaces for dealing with arbitrary runtime.Objects.
|
// Returns interfaces for dealing with arbitrary runtime.Objects.
|
||||||
Object() (meta.RESTMapper, runtime.ObjectTyper)
|
Object() (meta.RESTMapper, runtime.ObjectTyper)
|
||||||
// Returns interfaces for dealing with arbitrary
|
// Returns interfaces for dealing with arbitrary
|
||||||
@ -313,27 +317,36 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) Factory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clients := NewClientCache(clientConfig)
|
clients := NewClientCache(clientConfig)
|
||||||
return &factory{
|
|
||||||
|
f := &factory{
|
||||||
flags: flags,
|
flags: flags,
|
||||||
clientConfig: clientConfig,
|
clientConfig: clientConfig,
|
||||||
clients: clients,
|
clients: clients,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *factory) FlagSet() *pflag.FlagSet {
|
func (f *factory) FlagSet() *pflag.FlagSet {
|
||||||
return f.flags
|
return f.flags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *factory) Object() (meta.RESTMapper, runtime.ObjectTyper) {
|
func (f *factory) DiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||||
cfg, err := f.clientConfig.ClientConfig()
|
cfg, err := f.clientConfig.ClientConfig()
|
||||||
checkErrWithPrefix("failed to get client config: ", err)
|
if err != nil {
|
||||||
cmdApiVersion := unversioned.GroupVersion{}
|
return nil, err
|
||||||
if cfg.GroupVersion != nil {
|
|
||||||
cmdApiVersion = *cfg.GroupVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mapper := registered.RESTMapper()
|
|
||||||
discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg)
|
discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cacheDir := computeDiscoverCacheDir(filepath.Join(homedir.HomeDir(), ".kube", "cache", "discovery"), cfg.Host)
|
||||||
|
return NewCachedDiscoveryClient(discoveryClient, cacheDir, time.Duration(10*time.Minute)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *factory) Object() (meta.RESTMapper, runtime.ObjectTyper) {
|
||||||
|
mapper := registered.RESTMapper()
|
||||||
|
discoveryClient, err := f.DiscoveryClient()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
mapper = meta.FirstHitRESTMapper{
|
mapper = meta.FirstHitRESTMapper{
|
||||||
MultiRESTMapper: meta.MultiRESTMapper{
|
MultiRESTMapper: meta.MultiRESTMapper{
|
||||||
@ -345,23 +358,28 @@ func (f *factory) Object() (meta.RESTMapper, runtime.ObjectTyper) {
|
|||||||
|
|
||||||
// wrap with shortcuts
|
// wrap with shortcuts
|
||||||
mapper = NewShortcutExpander(mapper, discoveryClient)
|
mapper = NewShortcutExpander(mapper, discoveryClient)
|
||||||
|
|
||||||
// wrap with output preferences
|
// wrap with output preferences
|
||||||
|
cfg, err := f.clients.ClientConfigForVersion(nil)
|
||||||
|
checkErrWithPrefix("failed to get client config: ", err)
|
||||||
|
cmdApiVersion := unversioned.GroupVersion{}
|
||||||
|
if cfg.GroupVersion != nil {
|
||||||
|
cmdApiVersion = *cfg.GroupVersion
|
||||||
|
}
|
||||||
mapper = kubectl.OutputVersionMapper{RESTMapper: mapper, OutputVersions: []unversioned.GroupVersion{cmdApiVersion}}
|
mapper = kubectl.OutputVersionMapper{RESTMapper: mapper, OutputVersions: []unversioned.GroupVersion{cmdApiVersion}}
|
||||||
return mapper, api.Scheme
|
return mapper, api.Scheme
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *factory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, error) {
|
func (f *factory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, error) {
|
||||||
cfg, err := f.clients.ClientConfigForVersion(nil)
|
discoveryClient, err := f.DiscoveryClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
groupResources, err := discovery.GetAPIGroupResources(discoveryClient)
|
||||||
dc, err := discovery.NewDiscoveryClientForConfig(cfg)
|
if err != nil && !discoveryClient.Fresh() {
|
||||||
if err != nil {
|
discoveryClient.Invalidate()
|
||||||
return nil, nil, err
|
groupResources, err = discovery.GetAPIGroupResources(discoveryClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
groupResources, err := discovery.GetAPIGroupResources(dc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -378,11 +396,9 @@ func (f *factory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructured)
|
mapper := discovery.NewDeferredDiscoveryRESTMapper(discoveryClient, meta.InterfacesForUnstructured)
|
||||||
|
|
||||||
typer := discovery.NewUnstructuredObjectTyper(groupResources)
|
typer := discovery.NewUnstructuredObjectTyper(groupResources)
|
||||||
|
return NewShortcutExpander(mapper, discoveryClient), typer, nil
|
||||||
return NewShortcutExpander(mapper, dc), typer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *factory) RESTClient() (*restclient.RESTClient, error) {
|
func (f *factory) RESTClient() (*restclient.RESTClient, error) {
|
||||||
@ -1316,3 +1332,16 @@ func (f *factory) SuggestedPodTemplateResources() []unversioned.GroupResource {
|
|||||||
{Resource: "replicaset"},
|
{Resource: "replicaset"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// overlyCautiousIllegalFileCharacters matches characters that *might* not be supported. Windows is really restrictive, so this is really restrictive
|
||||||
|
var overlyCautiousIllegalFileCharacters = regexp.MustCompile(`[^(\w/\.)]`)
|
||||||
|
|
||||||
|
// computeDiscoverCacheDir takes the parentDir and the host and comes up with a "usually non-colliding" name.
|
||||||
|
func computeDiscoverCacheDir(parentDir, host string) string {
|
||||||
|
// strip the optional scheme from host if its there:
|
||||||
|
schemelessHost := strings.Replace(strings.Replace(host, "https://", "", 1), "http://", "", 1)
|
||||||
|
// now do a simple collapse of non-AZ09 characters. Collisions are possible but unlikely. Even if we do collide the problem is short lived
|
||||||
|
safeHost := overlyCautiousIllegalFileCharacters.ReplaceAllString(schemelessHost, "_")
|
||||||
|
|
||||||
|
return filepath.Join(parentDir, safeHost)
|
||||||
|
}
|
||||||
|
@ -25,18 +25,18 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/kubectl"
|
"k8s.io/kubernetes/pkg/kubectl"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ShortcutExpander is a RESTMapper that can be used for OpenShift resources. It expands the resource first, then invokes the wrapped
|
// ShortcutExpander is a RESTMapper that can be used for Kubernetes resources. It expands the resource first, then invokes the wrapped
|
||||||
type ShortcutExpander struct {
|
type ShortcutExpander struct {
|
||||||
RESTMapper meta.RESTMapper
|
RESTMapper meta.RESTMapper
|
||||||
|
|
||||||
All []unversioned.GroupResource
|
All []unversioned.GroupResource
|
||||||
|
|
||||||
discoveryClient *discovery.DiscoveryClient
|
discoveryClient discovery.DiscoveryInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ meta.RESTMapper = &ShortcutExpander{}
|
var _ meta.RESTMapper = &ShortcutExpander{}
|
||||||
|
|
||||||
func NewShortcutExpander(delegate meta.RESTMapper, client *discovery.DiscoveryClient) ShortcutExpander {
|
func NewShortcutExpander(delegate meta.RESTMapper, client discovery.DiscoveryInterface) ShortcutExpander {
|
||||||
return ShortcutExpander{All: userResources, RESTMapper: delegate, discoveryClient: client}
|
return ShortcutExpander{All: userResources, RESTMapper: delegate, discoveryClient: client}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user