From f940c42e563e67279cc28c7c129a340dc800fd30 Mon Sep 17 00:00:00 2001 From: Kiran Kamineni Date: Mon, 8 Apr 2019 16:36:34 -0700 Subject: Add support for generic plugin 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 --- src/k8splugin/go.mod | 2 +- src/k8splugin/go.sum | 1 + src/k8splugin/internal/app/client.go | 132 +++++++++++++++++++++++++++++++- src/k8splugin/plugins/generic/plugin.go | 109 ++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 src/k8splugin/plugins/generic/plugin.go diff --git a/src/k8splugin/go.mod b/src/k8splugin/go.mod index 29a10ecb..00ee2c8e 100644 --- a/src/k8splugin/go.mod +++ b/src/k8splugin/go.mod @@ -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 diff --git a/src/k8splugin/go.sum b/src/k8splugin/go.sum index f22fe8da..e5f34549 100644 --- a/src/k8splugin/go.sum +++ b/src/k8splugin/go.sum @@ -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= diff --git a/src/k8splugin/internal/app/client.go b/src/k8splugin/internal/app/client.go index cb6f0eca..cd1ec8a2 100644 --- a/src/k8splugin/internal/app/client.go +++ b/src/k8splugin/internal/app/client.go @@ -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 index 00000000..b0cf609c --- /dev/null +++ b/src/k8splugin/plugins/generic/plugin.go @@ -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 -- cgit 1.2.3-korg