2016-10-24 10:58:47 +00:00
/ *
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/golang/glog"
2017-05-26 21:00:24 +00:00
"github.com/googleapis/gnostic/OpenAPIv2"
2016-10-24 10:58:47 +00:00
2017-01-11 14:09:48 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
2017-01-16 11:05:15 +00:00
"k8s.io/apimachinery/pkg/version"
2017-01-25 19:00:30 +00:00
"k8s.io/client-go/discovery"
2017-01-19 18:27:59 +00:00
restclient "k8s.io/client-go/rest"
2017-11-02 21:58:59 +00:00
"k8s.io/kubernetes/pkg/kubectl/scheme"
2016-10-24 10:58:47 +00:00
)
// 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.
2016-12-03 18:57:26 +00:00
func ( d * CachedDiscoveryClient ) ServerResourcesForGroupVersion ( groupVersion string ) ( * metav1 . APIResourceList , error ) {
2016-10-24 10:58:47 +00:00
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 {
2016-12-03 18:57:26 +00:00
cachedResources := & metav1 . APIResourceList { }
2017-11-02 21:58:59 +00:00
if err := runtime . DecodeInto ( scheme . Codecs . UniversalDecoder ( ) , cachedBytes , cachedResources ) ; err == nil {
2017-10-12 04:27:42 +00:00
glog . V ( 10 ) . Infof ( "returning cached discovery info from %v" , filename )
2016-10-24 10:58:47 +00:00
return cachedResources , nil
}
}
liveResources , err := d . delegate . ServerResourcesForGroupVersion ( groupVersion )
if err != nil {
2017-02-28 18:15:25 +00:00
glog . V ( 3 ) . Infof ( "skipped caching discovery info due to %v" , err )
return liveResources , err
}
if liveResources == nil || len ( liveResources . APIResources ) == 0 {
glog . V ( 3 ) . Infof ( "skipped caching discovery info, no resources found" )
2016-10-24 10:58:47 +00:00
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.
2016-11-17 13:19:03 +00:00
func ( d * CachedDiscoveryClient ) ServerResources ( ) ( [ ] * metav1 . APIResourceList , error ) {
2016-10-24 10:58:47 +00:00
apiGroups , err := d . ServerGroups ( )
if err != nil {
return nil , err
}
2016-12-03 18:57:26 +00:00
groupVersions := metav1 . ExtractGroupVersions ( apiGroups )
2016-11-17 13:19:03 +00:00
result := [ ] * metav1 . APIResourceList { }
2016-10-24 10:58:47 +00:00
for _ , groupVersion := range groupVersions {
resources , err := d . ServerResourcesForGroupVersion ( groupVersion )
if err != nil {
return nil , err
}
2016-11-17 13:19:03 +00:00
result = append ( result , resources )
2016-10-24 10:58:47 +00:00
}
return result , nil
}
2016-12-03 18:57:26 +00:00
func ( d * CachedDiscoveryClient ) ServerGroups ( ) ( * metav1 . APIGroupList , error ) {
2016-10-24 10:58:47 +00:00
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 {
2016-12-03 18:57:26 +00:00
cachedGroups := & metav1 . APIGroupList { }
2017-11-02 21:58:59 +00:00
if err := runtime . DecodeInto ( scheme . Codecs . UniversalDecoder ( ) , cachedBytes , cachedGroups ) ; err == nil {
2017-10-12 04:27:42 +00:00
glog . V ( 10 ) . Infof ( "returning cached discovery info from %v" , filename )
2016-10-24 10:58:47 +00:00
return cachedGroups , nil
}
}
liveGroups , err := d . delegate . ServerGroups ( )
if err != nil {
2017-02-28 18:15:25 +00:00
glog . V ( 3 ) . Infof ( "skipped caching discovery info due to %v" , err )
return liveGroups , err
}
if liveGroups == nil || len ( liveGroups . Groups ) == 0 {
glog . V ( 3 ) . Infof ( "skipped caching discovery info, no groups found" )
2016-10-24 10:58:47 +00:00
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
}
2017-02-24 08:18:22 +00:00
defer file . Close ( )
2016-10-24 10:58:47 +00:00
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
}
2017-11-02 21:58:59 +00:00
bytes , err := runtime . Encode ( scheme . Codecs . LegacyCodec ( ) , obj )
2016-10-24 10:58:47 +00:00
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
}
2017-05-29 20:52:00 +00:00
err = os . Chmod ( f . Name ( ) , 0755 )
2016-10-24 10:58:47 +00:00
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 ( )
}
2016-11-17 13:19:03 +00:00
func ( d * CachedDiscoveryClient ) ServerPreferredResources ( ) ( [ ] * metav1 . APIResourceList , error ) {
2016-10-24 10:58:47 +00:00
return d . delegate . ServerPreferredResources ( )
}
2016-11-17 13:19:03 +00:00
func ( d * CachedDiscoveryClient ) ServerPreferredNamespacedResources ( ) ( [ ] * metav1 . APIResourceList , error ) {
2016-10-24 10:58:47 +00:00
return d . delegate . ServerPreferredNamespacedResources ( )
}
func ( d * CachedDiscoveryClient ) ServerVersion ( ) ( * version . Info , error ) {
return d . delegate . ServerVersion ( )
}
2017-05-26 21:00:24 +00:00
func ( d * CachedDiscoveryClient ) OpenAPISchema ( ) ( * openapi_v2 . Document , error ) {
2017-04-14 01:01:38 +00:00
return d . delegate . OpenAPISchema ( )
}
2016-10-24 10:58:47 +00:00
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 ,
}
}