Skip to content

Commit

Permalink
feat(factory): abstraction to generate clients and builders given the…
Browse files Browse the repository at this point in the history
… config and flags combination

Notice this code is from kubernetes source code.
Cannot be imported atm because of issues with go modules otherwise.
  • Loading branch information
leodido authored and fntlnz committed Nov 25, 2018
1 parent 11e3f9f commit 8970fd8
Show file tree
Hide file tree
Showing 3 changed files with 316 additions and 0 deletions.
197 changes: 197 additions & 0 deletions factory/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package factory

import (
"sync"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericclioptions/resource"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
openapivalidation "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi/validation"
"k8s.io/kubernetes/pkg/kubectl/validation"
)

// Factory provides abstractions that allow the kubectl command to be extended across multiple types of resources and different API sets.
// The rings are here for a reason.
// In order for composers to be able to provide alternative factory implementations
// they need to provide low level pieces of *certain* functions so that when the factory calls back into itself
// it uses the custom version of the function. Rather than try to enumerate everything that someone would want to override
// we split the factory into rings, where each ring can depend on methods in an earlier ring, but cannot depend
// upon peer methods in its own ring.
// TODO: make the functions interfaces
type Factory interface {
genericclioptions.RESTClientGetter

// DynamicClient returns a dynamic client ready for use
DynamicClient() (dynamic.Interface, error)

// KubernetesClientSet gives you back an external clientset
KubernetesClientSet() (*kubernetes.Clientset, error)

// Returns a RESTClient for accessing Kubernetes resources or an error.
RESTClient() (*restclient.RESTClient, error)

// NewBuilder returns an object that assists in loading objects from both disk and the server
// and which implements the common patterns for CLI interactions with generic resources.
NewBuilder() *resource.Builder

// Returns a RESTClient for working with the specified RESTMapping or an error. This is intended
// for working with arbitrary resources and is not guaranteed to point to a Kubernetes APIServer.
ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error)

// Returns a RESTClient for working with Unstructured objects.
UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error)

// Returns a schema that can validate objects stored on disk.
Validator(validate bool) (validation.Schema, error)

// OpenAPISchema returns the schema openapi schema definition
OpenAPISchema() (openapi.Resources, error)
}

type factoryImpl struct {
clientGetter genericclioptions.RESTClientGetter

// openAPIGetter loads and caches openapi specs
openAPIGetter openAPIGetter
}

type openAPIGetter struct {
once sync.Once
getter openapi.Getter
}

func NewFactory(clientGetter genericclioptions.RESTClientGetter) Factory {
if clientGetter == nil {
panic("attempt to instantiate client_access_factory with nil clientGetter")
}

f := &factoryImpl{
clientGetter: clientGetter,
}

return f
}

func (f *factoryImpl) ToRESTConfig() (*restclient.Config, error) {
return f.clientGetter.ToRESTConfig()
}

func (f *factoryImpl) ToRESTMapper() (meta.RESTMapper, error) {
return f.clientGetter.ToRESTMapper()
}

func (f *factoryImpl) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
return f.clientGetter.ToDiscoveryClient()
}

func (f *factoryImpl) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return f.clientGetter.ToRawKubeConfigLoader()
}

func (f *factoryImpl) KubernetesClientSet() (*kubernetes.Clientset, error) {
clientConfig, err := f.ToRESTConfig()
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(clientConfig)
}

func (f *factoryImpl) DynamicClient() (dynamic.Interface, error) {
clientConfig, err := f.ToRESTConfig()
if err != nil {
return nil, err
}
return dynamic.NewForConfig(clientConfig)
}

// NewBuilder returns a new resource builder for structured api objects.
func (f *factoryImpl) NewBuilder() *resource.Builder {
return resource.NewBuilder(f.clientGetter)
}

func (f *factoryImpl) RESTClient() (*restclient.RESTClient, error) {
clientConfig, err := f.ToRESTConfig()
if err != nil {
return nil, err
}
setKubernetesDefaults(clientConfig)
return restclient.RESTClientFor(clientConfig)
}

func (f *factoryImpl) ClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) {
cfg, err := f.clientGetter.ToRESTConfig()
if err != nil {
return nil, err
}
if err := setKubernetesDefaults(cfg); err != nil {
return nil, err
}
gvk := mapping.GroupVersionKind
switch gvk.Group {
case corev1.GroupName:
cfg.APIPath = "/api"
default:
cfg.APIPath = "/apis"
}
gv := gvk.GroupVersion()
cfg.GroupVersion = &gv
return restclient.RESTClientFor(cfg)
}

func (f *factoryImpl) UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) {
cfg, err := f.clientGetter.ToRESTConfig()
if err != nil {
return nil, err
}
if err := restclient.SetKubernetesDefaults(cfg); err != nil {
return nil, err
}
cfg.APIPath = "/apis"
if mapping.GroupVersionKind.Group == corev1.GroupName {
cfg.APIPath = "/api"
}
gv := mapping.GroupVersionKind.GroupVersion()
cfg.ContentConfig = resource.UnstructuredPlusDefaultContentConfig()
cfg.GroupVersion = &gv
return restclient.RESTClientFor(cfg)
}

func (f *factoryImpl) Validator(validate bool) (validation.Schema, error) {
if !validate {
return validation.NullSchema{}, nil
}

resources, err := f.OpenAPISchema()
if err != nil {
return nil, err
}

return validation.ConjunctiveSchema{
openapivalidation.NewSchemaValidation(resources),
validation.NoDoubleKeySchema{},
}, nil
}

// OpenAPISchema returns metadata and structural information about Kubernetes object definitions.
func (f *factoryImpl) OpenAPISchema() (openapi.Resources, error) {
discovery, err := f.clientGetter.ToDiscoveryClient()
if err != nil {
return nil, err
}

// Lazily initialize the OpenAPIGetter once
f.openAPIGetter.once.Do(func() {
// Create the caching OpenAPIGetter
f.openAPIGetter.getter = openapi.NewOpenAPIGetter(discovery)
})

// Delegate to the OpenAPIGetter
return f.openAPIGetter.getter.Get()
}
92 changes: 92 additions & 0 deletions factory/matchversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package factory

import (
"sync"

"github.com/spf13/pflag"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"

"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/kubernetes/pkg/version"
)

const (
flagMatchBinaryVersion = "match-server-version"
)

// MatchVersionFlags is for setting the "match server version" function.
type MatchVersionFlags struct {
Delegate genericclioptions.RESTClientGetter

RequireMatchedServerVersion bool
checkServerVersion sync.Once
matchesServerVersionErr error
}

var _ genericclioptions.RESTClientGetter = &MatchVersionFlags{}

func (f *MatchVersionFlags) checkMatchingServerVersion() error {
f.checkServerVersion.Do(func() {
if !f.RequireMatchedServerVersion {
return
}
discoveryClient, err := f.Delegate.ToDiscoveryClient()
if err != nil {
f.matchesServerVersionErr = err
return
}
f.matchesServerVersionErr = discovery.MatchesServerVersion(version.Get(), discoveryClient)
})

return f.matchesServerVersionErr
}

// ToRESTConfig implements RESTClientGetter.
// Returns a REST client configuration based on a provided path
// to a .kubeconfig file, loading rules, and config flag overrides.
// Expects the AddFlags method to have been called.
func (f *MatchVersionFlags) ToRESTConfig() (*rest.Config, error) {
if err := f.checkMatchingServerVersion(); err != nil {
return nil, err
}
clientConfig, err := f.Delegate.ToRESTConfig()
if err != nil {
return nil, err
}
// TODO we should not have to do this. It smacks of something going wrong.
setKubernetesDefaults(clientConfig)
return clientConfig, nil
}

func (f *MatchVersionFlags) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return f.Delegate.ToRawKubeConfigLoader()
}

func (f *MatchVersionFlags) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
if err := f.checkMatchingServerVersion(); err != nil {
return nil, err
}
return f.Delegate.ToDiscoveryClient()
}

// RESTMapper returns a mapper.
func (f *MatchVersionFlags) ToRESTMapper() (meta.RESTMapper, error) {
if err := f.checkMatchingServerVersion(); err != nil {
return nil, err
}
return f.Delegate.ToRESTMapper()
}

func (f *MatchVersionFlags) AddFlags(flags *pflag.FlagSet) {
flags.BoolVar(&f.RequireMatchedServerVersion, flagMatchBinaryVersion, f.RequireMatchedServerVersion, "Require server version to match client version")
}

func NewMatchVersionFlags(delegate genericclioptions.RESTClientGetter) *MatchVersionFlags {
return &MatchVersionFlags{
Delegate: delegate,
}
}
27 changes: 27 additions & 0 deletions factory/setk8sdefaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package factory

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
)

// setKubernetesDefaults sets default values on the provided client config for accessing the
// Kubernetes API or returns an error if any of the defaults are impossible or invalid.
// TODO this isn't what we want. Each clientset should be setting defaults as it sees fit.
func setKubernetesDefaults(config *rest.Config) error {
// TODO remove this hack. This is allowing the GetOptions to be serialized.
config.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"}

if config.APIPath == "" {
config.APIPath = "/api"
}
if config.NegotiatedSerializer == nil {
// This codec factory ensures the resources are not converted. Therefore, resources
// will not be round-tripped through internal versions. Defaulting does not happen
// on the client.
config.NegotiatedSerializer = &serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
}
return rest.SetKubernetesDefaults(config)
}

0 comments on commit 8970fd8

Please sign in to comment.