Skip to content

Commit

Permalink
Support delete operation in kubeops function (#1102)
Browse files Browse the repository at this point in the history
* Support delete operation in kubeops function

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>

* kubeops do not delete if res not present

Signed-off-by: Prasad Ghangal <prasad.ghangal@gmail.com>

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
PrasadG193 and mergify[bot] committed Sep 29, 2021
1 parent 491f0b2 commit aaa9048
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 39 deletions.
46 changes: 41 additions & 5 deletions pkg/function/kubeops.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ package function
import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"

kanister "github.com/kanisterio/kanister/pkg"
crv1alpha1 "github.com/kanisterio/kanister/pkg/apis/cr/v1alpha1"
"github.com/kanisterio/kanister/pkg/kube"
"github.com/kanisterio/kanister/pkg/param"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func init() {
Expand All @@ -39,6 +45,8 @@ const (
KubeOpsSpecArg = "spec"
// KubeOpsNamespaceArg provides resource namespace
KubeOpsNamespaceArg = "namespace"
// KubeOpsObjectReference specifies object details for delete operation
KubeOpsObjectReferenceArg = "objectReference"
// KubeOpsOperationArg is the kubeops operation needs to be executed
KubeOpsOperationArg = "operation"
)
Expand All @@ -52,7 +60,8 @@ func (*kubeops) Name() string {
func (crs *kubeops) Exec(ctx context.Context, tp param.TemplateParams, args map[string]interface{}) (map[string]interface{}, error) {
var spec, namespace string
var op kube.Operation
if err := Arg(args, KubeOpsSpecArg, &spec); err != nil {
var objRefArg crv1alpha1.ObjectReference
if err := OptArg(args, KubeOpsSpecArg, &spec, ""); err != nil {
return nil, err
}
if err := Arg(args, KubeOpsOperationArg, &op); err != nil {
Expand All @@ -61,8 +70,16 @@ func (crs *kubeops) Exec(ctx context.Context, tp param.TemplateParams, args map[
if err := OptArg(args, KubeOpsNamespaceArg, &namespace, metav1.NamespaceDefault); err != nil {
return nil, err
}
kubeopsOp := kube.NewKubectlOperations(spec, namespace)
objRef, err := kubeopsOp.Execute(op)
if ArgExists(args, KubeOpsObjectReferenceArg) {
if err := OptArg(args, KubeOpsObjectReferenceArg, &objRefArg, nil); err != nil {
return nil, err
}
}
dynCli, err := kube.NewDynamicClient()
if err != nil {
return nil, err
}
objRef, err := execKubeOperation(dynCli, op, namespace, spec, objRefArg)
if err != nil {
return nil, err
}
Expand All @@ -78,9 +95,28 @@ func (crs *kubeops) Exec(ctx context.Context, tp param.TemplateParams, args map[
return out, nil
}

func execKubeOperation(dynCli dynamic.Interface, op kube.Operation, namespace, spec string, objRef crv1alpha1.ObjectReference) (*crv1alpha1.ObjectReference, error) {
kubeopsOp := kube.NewKubectlOperations(dynCli)
switch op {
case kube.CreateOperation:
if len(spec) == 0 {
return nil, errors.New(fmt.Sprintf("spec cannot be empty for %s operation", kube.CreateOperation))
}
return kubeopsOp.Create(strings.NewReader(spec), namespace)
case kube.DeleteOperation:
if objRef.Name == "" ||
objRef.Group == "" ||
objRef.APIVersion == "" ||
objRef.Resource == "" {
return nil, errors.New(fmt.Sprintf("missing one or more required fields name/namespace/group/apiVersion/resource in objectReference for %s operation", kube.DeleteOperation))
}
return kubeopsOp.Delete(objRef, namespace)
}
return nil, errors.New(fmt.Sprintf("invalid operation '%s'", op))
}

func (*kubeops) RequiredArgs() []string {
return []string{
KubeOpsSpecArg,
KubeOpsOperationArg,
}
}
71 changes: 60 additions & 11 deletions pkg/function/kubeops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const (
deploySpec = `apiVersion: apps/v1
kind: Deployment
metadata:
#generateName: deployment-
name: test-deployment
spec:
replicas: 1
Expand Down Expand Up @@ -105,7 +104,7 @@ func (s *KubeOpsSuite) SetUpSuite(c *C) {

ns := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "kanisterkubetasktest-",
GenerateName: "kanisterkubeopstest-",
},
}
cns, err := s.kubeCli.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
Expand All @@ -131,7 +130,7 @@ func (s *KubeOpsSuite) TearDownSuite(c *C) {

func createPhase(namespace string) crv1alpha1.BlueprintPhase {
return crv1alpha1.BlueprintPhase{
Name: "create-in-ns",
Name: "createDeploy",
Func: KubeOpsFuncName,
Args: map[string]interface{}{
KubeOpsOperationArg: "create",
Expand All @@ -141,6 +140,24 @@ func createPhase(namespace string) crv1alpha1.BlueprintPhase {
}
}

func deletePhase(gvr schema.GroupVersionResource, name, namespace string) crv1alpha1.BlueprintPhase {
return crv1alpha1.BlueprintPhase{
Name: "deleteDeploy",
Func: KubeOpsFuncName,
Args: map[string]interface{}{
KubeOpsOperationArg: "delete",
KubeOpsNamespaceArg: namespace,
KubeOpsObjectReferenceArg: map[string]interface{}{
"apiVersion": gvr.Version,
"group": gvr.Group,
"resource": gvr.Resource,
"name": name,
"namespace": namespace,
},
},
}
}

func createInSpecsNsPhase(namespace string) crv1alpha1.BlueprintPhase {
return crv1alpha1.BlueprintPhase{
Name: "create-in-def-ns",
Expand Down Expand Up @@ -187,14 +204,6 @@ func (s *KubeOpsSuite) TestKubeOps(c *C) {
bp crv1alpha1.Blueprint
expResource resourceRef
}{
{
bp: newCreateResourceBlueprint(createPhase(s.namespace)),
expResource: resourceRef{
gvr: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
name: "test-deployment",
namespace: s.namespace,
},
},
{
bp: newCreateResourceBlueprint(createInSpecsNsPhase(s.namespace)),
expResource: resourceRef{
Expand Down Expand Up @@ -232,6 +241,46 @@ func (s *KubeOpsSuite) TestKubeOps(c *C) {
}
}

func (s *KubeOpsSuite) TestKubeOpsCreateWaitDelete(c *C) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
tp := param.TemplateParams{}
action := "test"
gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
deployName := "test-deployment"

bp := newCreateResourceBlueprint(createPhase(s.namespace),
waitDeployPhase(s.namespace, deployName),
deletePhase(gvr, deployName, s.namespace))
phases, err := kanister.GetPhases(bp, action, kanister.DefaultVersion, tp)
c.Assert(err, IsNil)
for _, p := range phases {
out, err := p.Exec(ctx, bp, action, tp)
c.Assert(err, IsNil, Commentf("Phase %s failed", p.Name()))

_, err = s.dynCli.Resource(gvr).Namespace(s.namespace).Get(context.TODO(), deployName, metav1.GetOptions{})
if p.Name() == "deleteDeploy" {
c.Assert(err, NotNil)
c.Assert(apierrors.IsNotFound(err), Equals, true)
} else {
c.Assert(err, IsNil)
}

if p.Name() == "waitDeployReady" {
continue
}
expOut := map[string]interface{}{
"apiVersion": gvr.Version,
"group": gvr.Group,
"resource": gvr.Resource,
"kind": "",
"name": deployName,
"namespace": s.namespace,
}
c.Assert(out, DeepEquals, expOut)
}
}

func getSampleCRD() *extensionsv1.CustomResourceDefinition {
return &extensionsv1.CustomResourceDefinition{
TypeMeta: metav1.TypeMeta{
Expand Down
57 changes: 34 additions & 23 deletions pkg/kube/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,57 +15,53 @@
package kube

import (
"context"
"io"
"strings"

"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
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/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic"
cmdutil "k8s.io/kubectl/pkg/cmd/util"

crv1alpha1 "github.com/kanisterio/kanister/pkg/apis/cr/v1alpha1"
)

// Operation represents kubectl operation
type Operation string

const (
// CreateOperation represents kubectl create operation
CreateOperation Operation = "create"
// DeleteOperation represents kubectl delete operation
DeleteOperation Operation = "delete"
)

// KubectlOperation implements methods to perform kubectl operations
type KubectlOperation struct {
factory cmdutil.Factory
spec io.Reader
namespace string
dynCli dynamic.Interface
factory cmdutil.Factory
}

// NewKubectlOperations returns new KubectlOperations object
func NewKubectlOperations(specString, namespace string) *KubectlOperation {
func NewKubectlOperations(dynCli dynamic.Interface) *KubectlOperation {
return &KubectlOperation{
factory: cmdutil.NewFactory(genericclioptions.NewConfigFlags(false)),
spec: strings.NewReader(specString),
namespace: namespace,
dynCli: dynCli,
factory: cmdutil.NewFactory(genericclioptions.NewConfigFlags(false)),
}
}

// Execute executes kubectl operation
func (k *KubectlOperation) Execute(op Operation) (*crv1alpha1.ObjectReference, error) {
switch op {
case CreateOperation:
return k.create()
default:
return nil, errors.New("not implemented")
}
}

func (k *KubectlOperation) create() (*crv1alpha1.ObjectReference, error) {
// Create k8s resource from spec manifest
func (k *KubectlOperation) Create(spec io.Reader, namespace string) (*crv1alpha1.ObjectReference, error) {
// TODO: Create namespace if doesn't exist before creating an resource
result := k.factory.NewBuilder().
Unstructured().
NamespaceParam(k.namespace).
Stream(k.spec, "resource").
NamespaceParam(namespace).
Stream(spec, "resource").
Flatten().
Do()
err := result.Err()
Expand All @@ -77,7 +73,6 @@ func (k *KubectlOperation) create() (*crv1alpha1.ObjectReference, error) {
if err != nil {
return err
}
namespace := k.namespace
// Override namespace if the namespace is set in resource spec
if info.Namespace != "" {
namespace = info.Namespace
Expand Down Expand Up @@ -106,3 +101,19 @@ func (k *KubectlOperation) create() (*crv1alpha1.ObjectReference, error) {
})
return objRef, err
}

// Delete k8s resource referred by objectReference
func (k *KubectlOperation) Delete(objRef crv1alpha1.ObjectReference, namespace string) (*crv1alpha1.ObjectReference, error) {
if namespace == "" {
namespace = metav1.NamespaceDefault
}
if objRef.Namespace != "" {
namespace = objRef.Namespace
}
err := k.dynCli.Resource(schema.GroupVersionResource{Group: objRef.Group, Version: objRef.APIVersion, Resource: objRef.Resource}).Namespace(namespace).Delete(context.Background(), objRef.Name, metav1.DeleteOptions{})
if apierrors.IsNotFound(err) {
return &objRef, nil
}

return &objRef, err
}

0 comments on commit aaa9048

Please sign in to comment.