Add support for generic plugin 64/84764/6
authorKiran Kamineni <kiran.k.kamineni@intel.com>
Mon, 8 Apr 2019 23:36:34 +0000 (16:36 -0700)
committerKiran Kamineni <kiran.k.kamineni@intel.com>
Wed, 10 Apr 2019 18:54:00 +0000 (11:54 -0700)
Add support for generic kinds
We are currently using a known map to find the
group version resource for a kind.

Issue-ID: MULTICLOUD-350
Change-Id: I5a64f73760b73cf92b9a3fab8c22ad54e0a5f84f
Signed-off-by: Kiran Kamineni <kiran.k.kamineni@intel.com>
src/k8splugin/go.mod
src/k8splugin/go.sum
src/k8splugin/internal/app/client.go
src/k8splugin/plugins/generic/plugin.go [new file with mode: 0644]

index 29a10ec..00ee2c8 100644 (file)
@@ -75,7 +75,7 @@ require (
        gopkg.in/square/go-jose.v2 v2.2.2 // indirect
        gopkg.in/yaml.v2 v2.2.1
        k8s.io/api v0.0.0-20181126151915-b503174bad59
-       k8s.io/apiextensions-apiserver v0.0.0-20181126155829-0cd23ebeb688 // indirect
+       k8s.io/apiextensions-apiserver v0.0.0-20181126155829-0cd23ebeb688
        k8s.io/apimachinery v0.0.0-20181126123746-eddba98df674
        k8s.io/apiserver v0.0.0-20181126153457-92fdef3a232a // indirect
        k8s.io/cli-runtime v0.0.0-20190107235426-31214e12222d // indirect
index f22fe8d..e5f3454 100644 (file)
@@ -257,6 +257,7 @@ k8s.io/client-go v9.0.0+incompatible h1:/PdJjifJTjMFe0G4ESclZDcwF1+bFePTJ2xf+MXj
 k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
 k8s.io/client-go v10.0.0+incompatible h1:h3fciHPG0O5QEzATTFoRw/YGtDsU6pxrMrAhxiTtcq0=
 k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
+k8s.io/client-go v11.0.0+incompatible h1:X3ykd+Z4G8MojP9TVDOR+h/IrpYJEolfR8W2B/FGKrk=
 k8s.io/helm v2.12.1+incompatible h1:Fw6it7ALJfqbbX95U3is3aswD6E8nh4aUYtvgzfna8A=
 k8s.io/helm v2.12.1+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI=
 k8s.io/helm v2.12.2+incompatible h1:vtddbkiGNMOd8maDDZDc111Rm9E5JeeNWDndows18i8=
index cb6f0ec..cd1ec8a 100644 (file)
@@ -21,13 +21,26 @@ import (
        utils "k8splugin/internal"
 
        pkgerrors "github.com/pkg/errors"
+       "k8s.io/apimachinery/pkg/api/meta"
+       "k8s.io/client-go/discovery"
+       "k8s.io/client-go/dynamic"
        "k8s.io/client-go/kubernetes"
+       "k8s.io/client-go/restmapper"
        "k8s.io/client-go/tools/clientcmd"
        "k8s.io/helm/pkg/tiller"
 )
 
+// KubernetesResource is the interface that is implemented
+type KubernetesResource interface {
+       Create(yamlFilePath string, namespace string, client *KubernetesClient) (string, error)
+       Delete(kind string, name string, namespace string, client *KubernetesClient) error
+}
+
 type KubernetesClient struct {
-       clientSet *kubernetes.Clientset
+       clientSet      *kubernetes.Clientset
+       dynamicClient  dynamic.Interface
+       discoverClient *discovery.DiscoveryClient
+       restMapper     meta.RESTMapper
 }
 
 // GetKubeClient loads the Kubernetes configuation values stored into the local configuration file
@@ -46,6 +59,16 @@ func (k *KubernetesClient) init(configPath string) error {
                return err
        }
 
+       k.dynamicClient, err = dynamic.NewForConfig(config)
+       if err != nil {
+               return pkgerrors.Wrap(err, "Creating dynamic client")
+       }
+
+       k.discoverClient, err = discovery.NewDiscoveryClientForConfig(config)
+       if err != nil {
+               return pkgerrors.Wrap(err, "Creating discovery client")
+       }
+
        return nil
 }
 
@@ -82,6 +105,50 @@ func (k *KubernetesClient) ensureNamespace(namespace string) error {
        return nil
 }
 
+func (k *KubernetesClient) createGeneric(kind string, files []string, namespace string) ([]string, error) {
+
+       log.Println("Processing items of Kind: " + kind)
+
+       //Check if have the mapper before loading the plugin
+       err := k.updateMapper()
+       if err != nil {
+               return nil, pkgerrors.Wrap(err, "Unable to create RESTMapper")
+       }
+
+       pluginObject, ok := utils.LoadedPlugins["generic"]
+       if !ok {
+               return nil, pkgerrors.New("No generic plugin found")
+       }
+
+       symbol, err := pluginObject.Lookup("ExportedVariable")
+       if err != nil {
+               return nil, pkgerrors.Wrap(err, "No ExportedVariable symbol found")
+       }
+
+       genericPlugin, ok := symbol.(KubernetesResource)
+       if !ok {
+               return nil, pkgerrors.New("ExportedVariable is not KubernetesResource type")
+       }
+
+       //Iterate over each file of a particular kind here
+       var resourcesCreated []string
+       for _, f := range files {
+               if _, err := os.Stat(f); os.IsNotExist(err) {
+                       return nil, pkgerrors.New("File " + f + "does not exists")
+               }
+
+               log.Println("Processing file: " + f)
+
+               name, err := genericPlugin.Create(f, namespace, k)
+               if err != nil {
+                       return nil, pkgerrors.Wrap(err, "Error in generic plugin")
+               }
+
+               resourcesCreated = append(resourcesCreated, name)
+       }
+       return resourcesCreated, nil
+}
+
 func (k *KubernetesClient) createKind(kind string, files []string, namespace string) ([]string, error) {
 
        log.Println("Processing items of Kind: " + kind)
@@ -103,7 +170,8 @@ func (k *KubernetesClient) createKind(kind string, files []string, namespace str
 
                typePlugin, ok := utils.LoadedPlugins[strings.ToLower(kind)]
                if !ok {
-                       return nil, pkgerrors.New("No plugin for kind " + kind + " found")
+                       log.Println("No plugin for kind " + kind + " found. Using generic Plugin")
+                       return k.createGeneric(kind, files, namespace)
                }
 
                symCreateResourceFunc, err := typePlugin.Lookup("Create")
@@ -163,17 +231,47 @@ func (k *KubernetesClient) createResources(resMap map[string][]string,
        return createdResourceMap, nil
 }
 
+func (k *KubernetesClient) deleteGeneric(kind string, resources []string, namespace string) error {
+       log.Println("Deleting items of Kind: " + kind)
+
+       pluginObject, ok := utils.LoadedPlugins["generic"]
+       if !ok {
+               return pkgerrors.New("No generic plugin found")
+       }
+
+       symbol, err := pluginObject.Lookup("ExportedVariable")
+       if err != nil {
+               return pkgerrors.Wrap(err, "No ExportedVariable symbol found")
+       }
+
+       //Assert that it implements the KubernetesResource
+       genericPlugin, ok := symbol.(KubernetesResource)
+       if !ok {
+               return pkgerrors.New("ExportedVariable is not KubernetesResource type")
+       }
+
+       for _, res := range resources {
+               err = genericPlugin.Delete(kind, res, namespace, k)
+               if err != nil {
+                       return pkgerrors.Wrap(err, "Error in generic plugin")
+               }
+       }
+
+       return nil
+}
+
 func (k *KubernetesClient) deleteKind(kind string, resources []string, namespace string) error {
        log.Println("Deleting items of Kind: " + kind)
 
        typePlugin, ok := utils.LoadedPlugins[strings.ToLower(kind)]
        if !ok {
-               return pkgerrors.New("No plugin for resource " + kind + " found")
+               log.Println("No plugin for kind " + kind + " found. Using generic Plugin")
+               return k.deleteGeneric(kind, resources, namespace)
        }
 
        symDeleteResourceFunc, err := typePlugin.Lookup("Delete")
        if err != nil {
-               return pkgerrors.Wrap(err, "Error fetching "+kind+" plugin")
+               return pkgerrors.Wrap(err, "Error findinf Delete symbol in plugin")
        }
 
        for _, res := range resources {
@@ -198,3 +296,29 @@ func (k *KubernetesClient) deleteResources(resMap map[string][]string, namespace
 
        return nil
 }
+
+func (k *KubernetesClient) updateMapper() error {
+       //Create restMapper if not already done
+       if k.restMapper != nil {
+               return nil
+       }
+
+       groupResources, err := restmapper.GetAPIGroupResources(k.discoverClient)
+       if err != nil {
+               return pkgerrors.Wrap(err, "Get GroupResources")
+       }
+
+       k.restMapper = restmapper.NewDiscoveryRESTMapper(groupResources)
+       return nil
+}
+
+//GetMapper returns the RESTMapper that was created for this client
+func (k *KubernetesClient) GetMapper() meta.RESTMapper {
+       return k.restMapper
+}
+
+//GetDynamicClient returns the dynamic client that is needed for
+//unstructured REST calls to the apiserver
+func (k *KubernetesClient) GetDynamicClient() dynamic.Interface {
+       return k.dynamicClient
+}
diff --git a/src/k8splugin/plugins/generic/plugin.go b/src/k8splugin/plugins/generic/plugin.go
new file mode 100644 (file)
index 0000000..b0cf609
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+Copyright 2018 Intel Corporation.
+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 main
+
+import (
+       "log"
+
+       pkgerrors "github.com/pkg/errors"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+       "k8s.io/apimachinery/pkg/runtime/schema"
+       "k8s.io/client-go/kubernetes/scheme"
+
+       utils "k8splugin/internal"
+       "k8splugin/internal/app"
+)
+
+type genericPlugin struct {
+}
+
+var kindToGVRMap = map[string]schema.GroupVersionResource{
+       "ConfigMap":   schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"},
+       "StatefulSet": schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"},
+       "Job":         schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"},
+       "Pod":         schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
+       "DaemonSet":   schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"},
+       "CustomResourceDefinition": schema.GroupVersionResource{
+               Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions",
+       },
+}
+
+// Create deployment object in a specific Kubernetes cluster
+func (g genericPlugin) Create(yamlFilePath string, namespace string, client *app.KubernetesClient) (string, error) {
+       if namespace == "" {
+               namespace = "default"
+       }
+
+       //Decode the yaml file to create a runtime.Object
+       obj, err := utils.DecodeYAML(yamlFilePath, nil)
+       if err != nil {
+               return "", pkgerrors.Wrap(err, "Decode deployment object error")
+       }
+
+       //Convert the runtime.Object to an unstructured Object
+       unstruct := &unstructured.Unstructured{}
+       err = scheme.Scheme.Convert(obj, unstruct, nil)
+       if err != nil {
+               return "", pkgerrors.Wrap(err, "Converting to unstructured object")
+       }
+
+       dynClient := client.GetDynamicClient()
+       mapper := client.GetMapper()
+
+       gvk := unstruct.GroupVersionKind()
+       mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version)
+       if err != nil {
+               return "", pkgerrors.Wrap(err, "Mapping kind to resource error")
+       }
+
+       gvr := mapping.Resource
+
+       createdObj, err := dynClient.Resource(gvr).Namespace(namespace).Create(unstruct, metav1.CreateOptions{})
+       if err != nil {
+               return "", pkgerrors.Wrap(err, "Create object error")
+       }
+
+       return createdObj.GetName(), nil
+}
+
+// Delete an existing deployment hosted in a specific Kubernetes cluster
+func (g genericPlugin) Delete(kind string, name string, namespace string, client *app.KubernetesClient) error {
+       if namespace == "" {
+               namespace = "default"
+       }
+
+       deletePolicy := metav1.DeletePropagationForeground
+       opts := &metav1.DeleteOptions{
+               PropagationPolicy: &deletePolicy,
+       }
+
+       dynClient := client.GetDynamicClient()
+       gvr, ok := kindToGVRMap[kind]
+       if !ok {
+               return pkgerrors.New("GVR not found for: " + kind)
+       }
+
+       log.Printf("Using gvr: %s, %s, %s", gvr.Group, gvr.Version, gvr.Resource)
+
+       err := dynClient.Resource(gvr).Namespace(namespace).Delete(name, opts)
+       if err != nil {
+               return pkgerrors.Wrap(err, "Delete object error")
+       }
+
+       return nil
+}
+
+// ExportedVariable is what we will look for when calling the generic plugin
+var ExportedVariable genericPlugin