From 4f61a018106bf21f03498d20fc9145df020a9668 Mon Sep 17 00:00:00 2001 From: Chuck Ha Date: Mon, 2 Jul 2018 20:47:17 -0400 Subject: [PATCH] Update code for new dynamic client * Fixes code consistency across commands * Adds new helper that glues together discovery and REST mapping Signed-off-by: Chuck Ha --- cmd/sonobuoy/app/common.go | 17 +++ cmd/sonobuoy/app/delete.go | 2 +- cmd/sonobuoy/app/e2e.go | 11 +- cmd/sonobuoy/app/gen.go | 10 +- cmd/sonobuoy/app/logs.go | 6 +- cmd/sonobuoy/app/retrieve.go | 7 +- cmd/sonobuoy/app/run.go | 21 ++-- cmd/sonobuoy/app/status.go | 5 +- pkg/client/example_interfaces_test.go | 9 +- pkg/client/gen_test.go | 2 +- pkg/client/interfaces.go | 24 ++-- pkg/client/run.go | 98 +++------------- pkg/dynamic/client.go | 98 ++++++++++++++++ pkg/dynamic/client_test.go | 158 ++++++++++++++++++++++++++ 14 files changed, 338 insertions(+), 130 deletions(-) create mode 100644 cmd/sonobuoy/app/common.go create mode 100644 pkg/dynamic/client.go create mode 100644 pkg/dynamic/client_test.go diff --git a/cmd/sonobuoy/app/common.go b/cmd/sonobuoy/app/common.go new file mode 100644 index 000000000..b40dcd070 --- /dev/null +++ b/cmd/sonobuoy/app/common.go @@ -0,0 +1,17 @@ +package app + +import ( + "github.com/heptio/sonobuoy/pkg/client" + sonodynamic "github.com/heptio/sonobuoy/pkg/dynamic" + + "github.com/pkg/errors" + "k8s.io/client-go/rest" +) + +func getSonobuoyClient(cfg *rest.Config) (*client.SonobuoyClient, error) { + skc, err := sonodynamic.NewAPIHelperFromRESTConfig(cfg) + if err != nil { + return nil, errors.Wrap(err, "couldn't get sonobuoy api helper") + } + return client.NewSonobuoyClient(cfg, skc) +} diff --git a/cmd/sonobuoy/app/delete.go b/cmd/sonobuoy/app/delete.go index bc46c9b4c..958fbf826 100644 --- a/cmd/sonobuoy/app/delete.go +++ b/cmd/sonobuoy/app/delete.go @@ -55,7 +55,7 @@ func deleteSonobuoyRun(cmd *cobra.Command, args []string) { os.Exit(1) } - sbc, err := client.NewSonobuoyClient(cfg) + sbc, err := getSonobuoyClient(cfg) if err != nil { errlog.LogError(errors.Wrap(err, "could not create sonobuoy client")) os.Exit(1) diff --git a/cmd/sonobuoy/app/e2e.go b/cmd/sonobuoy/app/e2e.go index 3f6a8ccc4..e36d62e1b 100644 --- a/cmd/sonobuoy/app/e2e.go +++ b/cmd/sonobuoy/app/e2e.go @@ -77,17 +77,16 @@ func e2es(cmd *cobra.Command, args []string) { } defer gzr.Close() - var restConfig *rest.Config + var cfg *rest.Config // If we are doing a rerun, only then, we need kubeconfig if e2eflags.rerun { - restConfig, err = e2eflags.kubecfg.Get() + cfg, err = e2eflags.kubecfg.Get() if err != nil { errlog.LogError(errors.Wrap(err, "couldn't get REST client")) os.Exit(1) } } - - sonobuoy, err := client.NewSonobuoyClient(restConfig) + sonobuoy, err := getSonobuoyClient(cfg) if err != nil { errlog.LogError(errors.Wrap(err, "could not create sonobuoy client")) os.Exit(1) @@ -105,7 +104,7 @@ func e2es(cmd *cobra.Command, args []string) { return } - cfg, err := e2eflags.Config() + runCfg, err := e2eflags.Config() if err != nil { errlog.LogError(errors.Wrap(err, "couldn't make a Run config")) os.Exit(1) @@ -125,7 +124,7 @@ func e2es(cmd *cobra.Command, args []string) { } fmt.Printf("Rerunning %d tests:\n", len(testCases)) - if err := sonobuoy.Run(cfg); err != nil { + if err := sonobuoy.Run(runCfg); err != nil { errlog.LogError(errors.Wrap(err, "error attempting to rerun failed tests")) os.Exit(1) } diff --git a/cmd/sonobuoy/app/gen.go b/cmd/sonobuoy/app/gen.go index 6510ce52b..755522857 100644 --- a/cmd/sonobuoy/app/gen.go +++ b/cmd/sonobuoy/app/gen.go @@ -95,13 +95,9 @@ func genManifest(cmd *cobra.Command, args []string) { errlog.LogError(err) os.Exit(1) } - // Passing in `nil` and no `kubeconfig` because it is not required by the method - // for generating any manifests - sbc, err := client.NewSonobuoyClient(nil) - if err != nil { - errlog.LogError(errors.Wrap(err, "could not create sonobuoy client")) - os.Exit(1) - } + + // Generate does not require any client configuration + sbc := &client.SonobuoyClient{} bytes, err := sbc.GenerateManifest(cfg) if err == nil { diff --git a/cmd/sonobuoy/app/logs.go b/cmd/sonobuoy/app/logs.go index b2e2c164e..bcff287df 100644 --- a/cmd/sonobuoy/app/logs.go +++ b/cmd/sonobuoy/app/logs.go @@ -54,12 +54,12 @@ func init() { } func getLogs(cmd *cobra.Command, args []string) { - restConfig, err := logsKubecfg.Get() + cfg, err := logsKubecfg.Get() if err != nil { - errlog.LogError(fmt.Errorf("failed to get rest config: %v", err)) + errlog.LogError(errors.Wrap(err, "failed to get rest config")) os.Exit(1) } - sbc, err := client.NewSonobuoyClient(restConfig) + sbc, err := getSonobuoyClient(cfg) if err != nil { errlog.LogError(errors.Wrap(err, "could not create sonobuoy client")) os.Exit(1) diff --git a/cmd/sonobuoy/app/retrieve.go b/cmd/sonobuoy/app/retrieve.go index 5c17e96b7..026283cfd 100644 --- a/cmd/sonobuoy/app/retrieve.go +++ b/cmd/sonobuoy/app/retrieve.go @@ -17,7 +17,6 @@ limitations under the License. package app import ( - "fmt" "os" "path/filepath" @@ -59,12 +58,12 @@ func retrieveResults(cmd *cobra.Command, args []string) { outDir = args[0] } - restConfig, err := rcvFlags.kubecfg.Get() + cfg, err := rcvFlags.kubecfg.Get() if err != nil { - errlog.LogError(fmt.Errorf("failed to get kubernetes client: %v", err)) + errlog.LogError(errors.Wrap(err, "failed to get kubernetes client")) os.Exit(1) } - sbc, err := client.NewSonobuoyClient(restConfig) + sbc, err := getSonobuoyClient(cfg) if err != nil { errlog.LogError(errors.Wrap(err, "could not create sonobuoy client")) os.Exit(1) diff --git a/cmd/sonobuoy/app/run.go b/cmd/sonobuoy/app/run.go index 131f040bc..21dd2039c 100644 --- a/cmd/sonobuoy/app/run.go +++ b/cmd/sonobuoy/app/run.go @@ -25,7 +25,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - ops "github.com/heptio/sonobuoy/pkg/client" + "github.com/heptio/sonobuoy/pkg/client" "github.com/heptio/sonobuoy/pkg/errlog" ) @@ -44,12 +44,12 @@ func RunFlagSet(cfg *runFlags) *pflag.FlagSet { return runset } -func (r *runFlags) Config() (*ops.RunConfig, error) { +func (r *runFlags) Config() (*client.RunConfig, error) { gencfg, err := r.genFlags.Config() if err != nil { return nil, err } - return &ops.RunConfig{ + return &client.RunConfig{ GenConfig: *gencfg, }, nil } @@ -67,26 +67,25 @@ func init() { } func submitSonobuoyRun(cmd *cobra.Command, args []string) { - restConfig, err := runflags.kubecfg.Get() + cfg, err := runflags.kubecfg.Get() if err != nil { errlog.LogError(errors.Wrap(err, "couldn't get REST client")) os.Exit(1) } - cfg, err := runflags.Config() + runCfg, err := runflags.Config() if err != nil { errlog.LogError(errors.Wrap(err, "could not retrieve E2E config")) os.Exit(1) } - - sbc, err := ops.NewSonobuoyClient(restConfig) + sbc, err := getSonobuoyClient(cfg) if err != nil { errlog.LogError(errors.Wrap(err, "could not create sonobuoy client")) os.Exit(1) } - plugins := make([]string, len(cfg.Config.PluginSelections)) - for i, plugin := range cfg.Config.PluginSelections { + plugins := make([]string, len(runCfg.Config.PluginSelections)) + for i, plugin := range runCfg.Config.PluginSelections { plugins[i] = plugin.Name } @@ -95,7 +94,7 @@ func submitSonobuoyRun(cmd *cobra.Command, args []string) { } if !runflags.skipPreflight { - if errs := sbc.PreflightChecks(&ops.PreflightConfig{Namespace: runflags.namespace}); len(errs) > 0 { + if errs := sbc.PreflightChecks(&client.PreflightConfig{Namespace: runflags.namespace}); len(errs) > 0 { errlog.LogError(errors.New("Preflight checks failed")) for _, err := range errs { errlog.LogError(err) @@ -104,7 +103,7 @@ func submitSonobuoyRun(cmd *cobra.Command, args []string) { } } - if err := sbc.Run(cfg); err != nil { + if err := sbc.Run(runCfg); err != nil { errlog.LogError(errors.Wrap(err, "error attempting to run sonobuoy")) os.Exit(1) } diff --git a/cmd/sonobuoy/app/status.go b/cmd/sonobuoy/app/status.go index fabd83381..41faedfcb 100644 --- a/cmd/sonobuoy/app/status.go +++ b/cmd/sonobuoy/app/status.go @@ -26,7 +26,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - ops "github.com/heptio/sonobuoy/pkg/client" "github.com/heptio/sonobuoy/pkg/errlog" "github.com/heptio/sonobuoy/pkg/plugin/aggregation" ) @@ -59,12 +58,12 @@ func init() { // TODO (timothysc) summarize and aggregate daemonset-plugins by status done (24) running (24) // also --show-all func getStatus(cmd *cobra.Command, args []string) { - config, err := statusFlags.kubecfg.Get() + cfg, err := statusFlags.kubecfg.Get() if err != nil { errlog.LogError(errors.Wrap(err, "couldn't get kubernetes config")) os.Exit(1) } - sbc, err := ops.NewSonobuoyClient(config) + sbc, err := getSonobuoyClient(cfg) if err != nil { errlog.LogError(errors.Wrap(err, "could not create sonobuoy client")) os.Exit(1) diff --git a/pkg/client/example_interfaces_test.go b/pkg/client/example_interfaces_test.go index 57dc76393..8764e7732 100644 --- a/pkg/client/example_interfaces_test.go +++ b/pkg/client/example_interfaces_test.go @@ -19,6 +19,7 @@ package client_test import ( "github.com/heptio/sonobuoy/pkg/client" "github.com/heptio/sonobuoy/pkg/config" + "github.com/heptio/sonobuoy/pkg/dynamic" "k8s.io/client-go/rest" ) @@ -27,8 +28,14 @@ var cfg *rest.Config // Example shows how to create a client and run Sonobuoy. func Example() { + // Get an APIHelper with default implementations from client-go. + apiHelper, err := dynamic.NewAPIHelperFromRESTConfig(cfg) + if err != nil { + panic(err) + } + // client.NewSonobuoyClient returns a struct that implements the client.Interface. - sonobuoy, err := client.NewSonobuoyClient(cfg) + sonobuoy, err := client.NewSonobuoyClient(cfg, apiHelper) if err != nil { panic(err) } diff --git a/pkg/client/gen_test.go b/pkg/client/gen_test.go index fcd485fd7..97762eb22 100644 --- a/pkg/client/gen_test.go +++ b/pkg/client/gen_test.go @@ -82,7 +82,7 @@ func TestGenerateManifest(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - sbc, err := client.NewSonobuoyClient(nil) + sbc, err := client.NewSonobuoyClient(nil, nil) if err != nil { t.Fatal(err) } diff --git a/pkg/client/interfaces.go b/pkg/client/interfaces.go index f810884b5..b50a9ab81 100644 --- a/pkg/client/interfaces.go +++ b/pkg/client/interfaces.go @@ -22,7 +22,7 @@ import ( "github.com/heptio/sonobuoy/pkg/config" "github.com/heptio/sonobuoy/pkg/plugin/aggregation" "github.com/pkg/errors" - "k8s.io/client-go/dynamic" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) @@ -78,19 +78,27 @@ type PreflightConfig struct { Namespace string } +// SonobuoyKubeAPIClient is the interface Sonobuoy uses to communicate with a kube-apiserver. +type SonobuoyKubeAPIClient interface { + CreateObject(*unstructured.Unstructured) (*unstructured.Unstructured, error) + Name(*unstructured.Unstructured) (string, error) + Namespace(*unstructured.Unstructured) (string, error) + ResourceVersion(*unstructured.Unstructured) (string, error) +} + // SonobuoyClient is a high-level interface to Sonobuoy operations. type SonobuoyClient struct { RestConfig *rest.Config client kubernetes.Interface - dynamicClient dynamic.ClientPool + dynamicClient SonobuoyKubeAPIClient } // NewSonobuoyClient creates a new SonobuoyClient -func NewSonobuoyClient(restConfig *rest.Config) (*SonobuoyClient, error) { +func NewSonobuoyClient(restConfig *rest.Config, skc SonobuoyKubeAPIClient) (*SonobuoyClient, error) { sc := &SonobuoyClient{ RestConfig: restConfig, client: nil, - dynamicClient: nil, + dynamicClient: skc, } return sc, nil } @@ -107,14 +115,6 @@ func (s *SonobuoyClient) Client() (kubernetes.Interface, error) { return s.client, nil } -// DynamicClientPool creates or retrieves an existing dynamic client from the SonobuoyClient's RESTConfig. -func (s *SonobuoyClient) DynamicClientPool() dynamic.ClientPool { - if s.dynamicClient == nil { - s.dynamicClient = dynamic.NewDynamicClientPool(s.RestConfig) - } - return s.dynamicClient -} - // Make sure SonobuoyClient implements the interface var _ Interface = &SonobuoyClient{} diff --git a/pkg/client/run.go b/pkg/client/run.go index b68764337..74ffef370 100644 --- a/pkg/client/run.go +++ b/pkg/client/run.go @@ -23,16 +23,10 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" kubeerror "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" ) const bufferSize = 4096 @@ -44,12 +38,6 @@ func (c *SonobuoyClient) Run(cfg *RunConfig) error { } buf := bytes.NewBuffer(manifest) - - mapper, err := newMapper(c.RestConfig) - if err != nil { - return errors.Wrap(err, "couldn't retrieve API spec from server") - } - d := yaml.NewYAMLOrJSONDecoder(buf, bufferSize) for { @@ -67,44 +55,32 @@ func (c *SonobuoyClient) Run(cfg *RunConfig) error { continue } - obj := unstructured.Unstructured{} - if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), ext.Raw, &obj); err != nil { + obj := &unstructured.Unstructured{} + if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), ext.Raw, obj); err != nil { return errors.Wrap(err, "couldn't decode template") } - - err := createObject(c.DynamicClientPool(), &obj, mapper) + name, err := c.dynamicClient.Name(obj) if err != nil { + return errors.Wrap(err, "could not get object name") + } + namespace, err := c.dynamicClient.Namespace(obj) + if err != nil { + return errors.Wrap(err, "could not get object namespace") + } + // err is used to determine output for user; but first extract resource + newObj, err := c.dynamicClient.CreateObject(obj) + resource, err2 := c.dynamicClient.ResourceVersion(newObj) + if err2 != nil { + return errors.Wrap(err, "could not get resource of object") + } + if err := handleCreateError(name, namespace, resource, err); err != nil { return errors.Wrap(err, "failed to create object") } } return nil } -func createObject(pool dynamic.ClientPool, obj *unstructured.Unstructured, mapper meta.RESTMapper) error { - client, err := pool.ClientForGroupVersionKind(obj.GroupVersionKind()) - if err != nil { - return errors.Wrap(err, "could not make kubernetes client") - } - - mapping, err := mapper.RESTMapping( - obj.GroupVersionKind().GroupKind(), - obj.GroupVersionKind().Version, - ) - if err != nil { - return errors.Wrap(err, "could not get resource for object") - } - resource := mapping.Resource - - name, namespace, err := getNames(obj) - if err != nil { - return errors.Wrap(err, "couldn't retrive object metadata") - } - - _, err = client.Resource(&metav1.APIResource{ - Name: resource, - Namespaced: namespace != "", - }, namespace).Create(obj) - +func handleCreateError(name, namespace, resource string, err error) error { log := logrus.WithFields(logrus.Fields{ "name": name, "namespace": namespace, @@ -122,45 +98,5 @@ func createObject(pool dynamic.ClientPool, obj *unstructured.Unstructured, mappe case err != nil: return errors.Wrapf(err, "failed to create API resource %s", name) } - return nil } - -func newMapper(cfg *rest.Config) (meta.RESTMapper, error) { - client, err := discovery.NewDiscoveryClientForConfig(cfg) - if err != nil { - return nil, errors.Wrap(err, "couldn't create discovery client") - } - resources, err := discovery.GetAPIGroupResources(client) - if err != nil { - return nil, errors.Wrap(err, "couldn't retrieve API resources from server") - } - - return discovery.NewRESTMapper( - resources, - unstructuredVersionInterface, - ), nil -} - -func getNames(obj runtime.Object) (string, string, error) { - accessor := meta.NewAccessor() - name, err := accessor.Name(obj) - if err != nil { - return "", "", errors.Wrapf(err, "couldn't get name for object %T", obj) - } - - namespace, err := accessor.Namespace(obj) - if err != nil { - return "", "", errors.Wrapf(err, "couldn't get namespace for object %s", name) - } - - return name, namespace, nil -} - -// implements meta.VersionInterfacesFunc -func unstructuredVersionInterface(version schema.GroupVersion) (*meta.VersionInterfaces, error) { - return &meta.VersionInterfaces{ - ObjectConvertor: &unstructured.UnstructuredObjectConverter{}, - MetadataAccessor: meta.NewAccessor(), - }, nil -} diff --git a/pkg/dynamic/client.go b/pkg/dynamic/client.go new file mode 100644 index 000000000..d61b5cf64 --- /dev/null +++ b/pkg/dynamic/client.go @@ -0,0 +1,98 @@ +package dynamic + +import ( + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" +) + +// A scoped down meta.MetadataAccessor +type MetadataAccessor interface { + Namespace(runtime.Object) (string, error) + Name(runtime.Object) (string, error) + ResourceVersion(runtime.Object) (string, error) +} + +// A scoped down meta.RESTMapper +type mapper interface { + RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) +} + +// APIHelper wraps the client-go dynamic client and exposes a simple interface. +type APIHelper struct { + Client dynamic.Interface + Mapper mapper + Accessor MetadataAccessor +} + +// NewAPIHelperFromRESTConfig creates a new APIHelper with default objects +// from client-go. +func NewAPIHelperFromRESTConfig(cfg *rest.Config) (*APIHelper, error) { + dynClient, err := dynamic.NewForConfig(cfg) + if err != nil { + return nil, errors.Wrap(err, "could not create dynamic client") + } + discover, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return nil, errors.Wrap(err, "could not create discovery client") + } + groupResources, err := restmapper.GetAPIGroupResources(discover) + if err != nil { + return nil, errors.Wrap(err, "could not get api group resources") + } + mapper := restmapper.NewDiscoveryRESTMapper(groupResources) + return NewAPIHelper(dynClient, mapper, meta.NewAccessor()) +} + +// NewAPIHelper returns an APIHelper with the internals instantiated. +func NewAPIHelper(dyn dynamic.Interface, mapper mapper, accessor MetadataAccessor) (*APIHelper, error) { + return &APIHelper{ + Client: dyn, + Mapper: mapper, + Accessor: accessor, + }, nil +} + +// CreateObject attempts to create any kubernetes object. +func (a *APIHelper) CreateObject(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + restMapping, err := a.Mapper.RESTMapping(obj.GroupVersionKind().GroupKind(), obj.GroupVersionKind().Version) + if err != nil { + return nil, errors.Wrap(err, "could not get restMapping") + } + name, err := a.Accessor.Name(obj) + if err != nil { + return nil, errors.Wrap(err, "could not get name for object") + } + namespace, err := a.Accessor.Namespace(obj) + if err != nil { + return nil, errors.Wrapf(err, "couldn't get namespace for object %s", name) + } + + rsc := a.Client.Resource(restMapping.Resource) + if rsc == nil { + return nil, errors.New("failed to get a resource interface") + } + ri := rsc.Namespace(namespace) + return ri.Create(obj) +} + +// Name returns the name of the kubernetes object. +func (a *APIHelper) Name(obj *unstructured.Unstructured) (string, error) { + return a.Accessor.Name(obj) +} + +// Namespace returns the namespace of the kubernetes object. +func (a *APIHelper) Namespace(obj *unstructured.Unstructured) (string, error) { + return a.Accessor.Namespace(obj) +} + +// ResourceVersion returns the resource version of a kubernetes object. +func (a *APIHelper) ResourceVersion(obj *unstructured.Unstructured) (string, error) { + return a.Accessor.ResourceVersion(obj) +} diff --git a/pkg/dynamic/client_test.go b/pkg/dynamic/client_test.go new file mode 100644 index 000000000..46d6905bc --- /dev/null +++ b/pkg/dynamic/client_test.go @@ -0,0 +1,158 @@ +package dynamic_test + +import ( + "errors" + "testing" + + sonodynamic "github.com/heptio/sonobuoy/pkg/dynamic" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" +) + +type testMapper struct{} + +func (t *testMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { + if gk.Kind == "fail-rest-mapping" { + return nil, errors.New("some error") + } + if gk.Kind == "fail-resource" { + return &meta.RESTMapping{ + Resource: schema.GroupVersionResource{ + Group: gk.Group, + Version: versions[0], + Resource: "fail-resource", + }, + }, nil + } + return &meta.RESTMapping{}, nil +} + +type testMetadataAccessor struct{} + +func (t *testMetadataAccessor) Namespace(obj runtime.Object) (string, error) { + if obj.GetObjectKind().GroupVersionKind().Kind == "fail-namespace" { + return "", errors.New("namespace error") + } + return "", nil +} +func (t *testMetadataAccessor) Name(obj runtime.Object) (string, error) { + if obj.GetObjectKind().GroupVersionKind().Kind == "fail-name" { + return "", errors.New("name error") + } + return "", nil +} +func (t *testMetadataAccessor) ResourceVersion(obj runtime.Object) (string, error) { return "", nil } + +type testDyanmicInterface struct{} + +func (t *testDyanmicInterface) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface { + if resource.String() == "testing/v1alpha1, Resource=fail-resource" { + return nil + } + return &testNamespaceableResourceInterface{} +} + +type testResourceInterface struct{} +type testNamespaceableResourceInterface struct { + testResourceInterface +} + +func (t *testNamespaceableResourceInterface) Namespace(string) dynamic.ResourceInterface { + return &testResourceInterface{} +} +func (t *testResourceInterface) Create(obj *unstructured.Unstructured, subresources ...string) (*unstructured.Unstructured, error) { + return obj, nil +} +func (t *testResourceInterface) Update(obj *unstructured.Unstructured, subresources ...string) (*unstructured.Unstructured, error) { + return nil, nil +} +func (t *testResourceInterface) UpdateStatus(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + return nil, nil +} +func (t *testResourceInterface) Delete(name string, options *metav1.DeleteOptions, subresources ...string) error { + return nil +} +func (t *testResourceInterface) DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error { + return nil +} +func (t *testResourceInterface) Get(name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { + return nil, nil +} +func (t *testResourceInterface) List(opts metav1.ListOptions) (*unstructured.UnstructuredList, error) { + return nil, nil +} +func (t *testResourceInterface) Watch(opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} +func (t *testResourceInterface) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (*unstructured.Unstructured, error) { + return nil, nil +} + +func TestCreateObject(t *testing.T) { + testcases := []struct { + name string + kind string + expectError bool + }{ + { + name: "simple passing test", + expectError: false, + }, + { + name: "there should be an error if restmapping fails", + kind: "fail-rest-mapping", + expectError: true, + }, + { + name: "there should be an error if the namespace accessor fails", + kind: "fail-namespace", + expectError: true, + }, + { + name: "there should be an error if name accessor fails", + kind: "fail-name", + expectError: true, + }, + { + name: "there should be an error if Resource fails to return a NamespacableResourceInterface", + kind: "fail-resource", + expectError: true, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + + helper, err := sonodynamic.NewAPIHelper(&testDyanmicInterface{}, &testMapper{}, &testMetadataAccessor{}) + if err != nil { + t.Fatalf("could not create apihelper: %v", err) + } + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "testing/v1alpha1", + "kind": tc.kind, + }, + } + out, err := helper.CreateObject(obj) + if tc.expectError && err == nil { + t.Fatalf("expected an error but got nil") + } + // return early if we got what we wanted + if tc.expectError && err != nil { + return + } + if err != nil { + t.Fatalf("failed to create object: %v", err) + } + if out == nil { + t.Fatalf("out should not be nil") + } + }) + + } +}