diff --git a/pkg/function/kubeops.go b/pkg/function/kubeops.go index 4170370598..550e3f7236 100644 --- a/pkg/function/kubeops.go +++ b/pkg/function/kubeops.go @@ -79,7 +79,7 @@ func (crs *kubeops) Exec(ctx context.Context, tp param.TemplateParams, args map[ if err != nil { return nil, err } - objRef, err := execKubeOperation(dynCli, op, namespace, spec, objRefArg) + objRef, err := execKubeOperation(ctx, dynCli, op, namespace, spec, objRefArg) if err != nil { return nil, err } @@ -95,7 +95,7 @@ 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) { +func execKubeOperation(ctx context.Context, dynCli dynamic.Interface, op kube.Operation, namespace, spec string, objRef crv1alpha1.ObjectReference) (*crv1alpha1.ObjectReference, error) { kubeopsOp := kube.NewKubectlOperations(dynCli) switch op { case kube.CreateOperation: @@ -105,12 +105,11 @@ func execKubeOperation(dynCli dynamic.Interface, op kube.Operation, namespace, s 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 kubeopsOp.Delete(ctx, objRef, namespace) } return nil, errors.New(fmt.Sprintf("invalid operation '%s'", op)) } diff --git a/pkg/function/kubeops_test.go b/pkg/function/kubeops_test.go index 6907ba9916..a186661593 100644 --- a/pkg/function/kubeops_test.go +++ b/pkg/function/kubeops_test.go @@ -128,14 +128,14 @@ func (s *KubeOpsSuite) TearDownSuite(c *C) { _ = s.crdCli.ApiextensionsV1().CustomResourceDefinitions().Delete(context.TODO(), getSampleCRD().GetName(), metav1.DeleteOptions{}) } -func createPhase(namespace string) crv1alpha1.BlueprintPhase { +func createPhase(namespace string, spec string) crv1alpha1.BlueprintPhase { return crv1alpha1.BlueprintPhase{ Name: "createDeploy", Func: KubeOpsFuncName, Args: map[string]interface{}{ KubeOpsOperationArg: "create", KubeOpsNamespaceArg: namespace, - KubeOpsSpecArg: deploySpec, + KubeOpsSpecArg: spec, }, } } @@ -239,6 +239,47 @@ func (s *KubeOpsSuite) TestKubeOps(c *C) { c.Assert(out, DeepEquals, expOut) } } + gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"} + serviceName := "test-deployment-2" + err := s.dynCli.Resource(gvr).Namespace(s.namespace).Delete(ctx, serviceName, metav1.DeleteOptions{}) + c.Assert(err, IsNil) +} + +func (s *KubeOpsSuite) TestKubeOpsCreateDeleteWithCoreResource(c *C) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + tp := param.TemplateParams{} + action := "test" + gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"} + serviceName := "test-deployment-2" + spec := fmt.Sprintf(serviceSpec, s.namespace) + + bp := newCreateResourceBlueprint(createPhase(s.namespace, spec), + deletePhase(gvr, serviceName, 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(ctx, serviceName, metav1.GetOptions{}) + if p.Name() == "deleteDeploy" { + c.Assert(err, NotNil) + c.Assert(apierrors.IsNotFound(err), Equals, true) + } else { + c.Assert(err, IsNil) + } + + expOut := map[string]interface{}{ + "apiVersion": gvr.Version, + "group": gvr.Group, + "resource": gvr.Resource, + "kind": "", + "name": serviceName, + "namespace": s.namespace, + } + c.Assert(out, DeepEquals, expOut) + } } func (s *KubeOpsSuite) TestKubeOpsCreateWaitDelete(c *C) { @@ -249,7 +290,7 @@ func (s *KubeOpsSuite) TestKubeOpsCreateWaitDelete(c *C) { gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} deployName := "test-deployment" - bp := newCreateResourceBlueprint(createPhase(s.namespace), + bp := newCreateResourceBlueprint(createPhase(s.namespace, deploySpec), waitDeployPhase(s.namespace, deployName), deletePhase(gvr, deployName, s.namespace)) phases, err := kanister.GetPhases(bp, action, kanister.DefaultVersion, tp) diff --git a/pkg/kube/kubectl.go b/pkg/kube/kubectl.go index 6f7c6d230e..c5e1385fcd 100644 --- a/pkg/kube/kubectl.go +++ b/pkg/kube/kubectl.go @@ -29,6 +29,7 @@ import ( cmdutil "k8s.io/kubectl/pkg/cmd/util" crv1alpha1 "github.com/kanisterio/kanister/pkg/apis/cr/v1alpha1" + "github.com/kanisterio/kanister/pkg/poll" ) // Operation represents kubectl operation @@ -102,18 +103,29 @@ func (k *KubectlOperation) Create(spec io.Reader, namespace string) (*crv1alpha1 return objRef, err } -// Delete k8s resource referred by objectReference -func (k *KubectlOperation) Delete(objRef crv1alpha1.ObjectReference, namespace string) (*crv1alpha1.ObjectReference, error) { +// Delete k8s resource referred by objectReference. Waits for the resource to be deleted +func (k *KubectlOperation) Delete(ctx context.Context, 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 + err := k.dynCli.Resource(schema.GroupVersionResource{Group: objRef.Group, Version: objRef.APIVersion, Resource: objRef.Resource}).Namespace(namespace).Delete(ctx, objRef.Name, metav1.DeleteOptions{}) + if err != nil { + return &objRef, err } + return waitForResourceDeletion(ctx, k, objRef, namespace) +} +// waitForResourceDeletion repeatedly checks for NotFound error after fetching the resource +func waitForResourceDeletion(ctx context.Context, k *KubectlOperation, objRef crv1alpha1.ObjectReference, namespace string) (*crv1alpha1.ObjectReference, error) { + err := poll.Wait(ctx, func(context.Context) (done bool, err error) { + _, err = k.dynCli.Resource(schema.GroupVersionResource{Group: objRef.Group, Version: objRef.APIVersion, Resource: objRef.Resource}).Namespace(namespace).Get(ctx, objRef.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + }) return &objRef, err }