You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by ra...@apache.org on 2020/09/08 17:18:01 UTC

[openwhisk] branch master updated: Force Delete Non-Empty Packages (#4941)

This is an automated email from the ASF dual-hosted git repository.

rabbah pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk.git


The following commit(s) were added to refs/heads/master by this push:
     new b70ea5a  Force Delete Non-Empty Packages (#4941)
b70ea5a is described below

commit b70ea5a908dc086eab7828b8b0a8d5131705c82b
Author: Brendan Doyle <bj...@georgetown.edu>
AuthorDate: Tue Sep 8 10:17:42 2020 -0700

    Force Delete Non-Empty Packages (#4941)
    
    Co-authored-by: Brendan Doyle <br...@qualtrics.com>
---
 .../src/main/resources/apiv1swagger.json           | 11 ++++
 .../openwhisk/core/controller/Packages.scala       | 69 ++++++++++++++--------
 docs/rest_api.md                                   |  9 +++
 .../core/controller/test/PackagesApiTests.scala    | 26 +++++++-
 4 files changed, 90 insertions(+), 25 deletions(-)

diff --git a/core/controller/src/main/resources/apiv1swagger.json b/core/controller/src/main/resources/apiv1swagger.json
index 9befc7c..390ca38 100644
--- a/core/controller/src/main/resources/apiv1swagger.json
+++ b/core/controller/src/main/resources/apiv1swagger.json
@@ -1440,6 +1440,17 @@
             "description": "Name of package",
             "required": true,
             "type": "string"
+          },
+          {
+            "name": "force",
+            "in": "query",
+            "description": "Force delete a package if it contains entities removing everything in it. Default is false.",
+            "required": false,
+            "type": "string",
+            "enum": [
+              "true",
+              "false"
+            ]
           }
         ],
         "responses": {
diff --git a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Packages.scala b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Packages.scala
index ffc992f..c524e95 100644
--- a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Packages.scala
+++ b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Packages.scala
@@ -117,7 +117,8 @@ trait WhiskPackagesApi extends WhiskCollectionAPI with ReferencedEntities {
   }
 
   /**
-   * Deletes package/binding. If a package, may only be deleted if there are no entities in the package.
+   * Deletes package/binding. If a package, may only be deleted if there are no entities in the package
+   * or force parameter is set to true, which will delete all contents of the package before deleting.
    *
    * Responses are one of (Code, Message)
    * - 200 WhiskPackage as JSON
@@ -126,29 +127,51 @@ trait WhiskPackagesApi extends WhiskCollectionAPI with ReferencedEntities {
    * - 500 Internal Server Error
    */
   override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
-    deleteEntity(
-      WhiskPackage,
-      entityStore,
-      entityName.toDocId,
-      (wp: WhiskPackage) => {
-        wp.binding map {
-          // this is a binding, it is safe to remove
-          _ =>
-            Future.successful({})
-        } getOrElse {
-          // may only delete a package if all its ingredients are deleted already
-          WhiskAction
-            .listCollectionInNamespace(entityStore, wp.namespace.addPath(wp.name), skip = 0, limit = 0) flatMap {
-            case Left(list) if (list.size != 0) =>
-              Future failed {
-                RejectRequest(
-                  Conflict,
-                  s"Package not empty (contains ${list.size} ${if (list.size == 1) "entity" else "entities"})")
-              }
-            case _ => Future.successful({})
+    parameter('force ? false) { force =>
+      deleteEntity(
+        WhiskPackage,
+        entityStore,
+        entityName.toDocId,
+        (wp: WhiskPackage) => {
+          wp.binding map {
+            // this is a binding, it is safe to remove
+            _ =>
+              Future.successful({})
+          } getOrElse {
+            // may only delete a package if all its ingredients are deleted already or force flag is set
+            WhiskAction
+              .listCollectionInNamespace(
+                entityStore,
+                wp.namespace.addPath(wp.name),
+                includeDocs = true,
+                skip = 0,
+                limit = 0) flatMap {
+              case Right(list) if list.nonEmpty && force =>
+                Future sequence {
+                  list.map(action => {
+                    WhiskAction.get(
+                      entityStore,
+                      wp.fullyQualifiedName(false)
+                        .add(action.fullyQualifiedName(false).name)
+                        .toDocId) flatMap { actionWithRevision =>
+                      WhiskAction.del(entityStore, actionWithRevision.docinfo)
+                    }
+                  })
+                } flatMap { _ =>
+                  Future.successful({})
+                }
+              case Right(list) if list.nonEmpty && !force =>
+                Future failed {
+                  RejectRequest(
+                    Conflict,
+                    s"Package not empty (contains ${list.size} ${if (list.size == 1) "entity" else "entities"}). Set force param or delete package contents.")
+                }
+              case _ =>
+                Future.successful({})
+            }
           }
-        }
-      })
+        })
+    }
   }
 
   /**
diff --git a/docs/rest_api.md b/docs/rest_api.md
index f7e7f4b..effbbd4 100644
--- a/docs/rest_api.md
+++ b/docs/rest_api.md
@@ -316,6 +316,15 @@ curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/packages/iot?overwrite=true \
 -d '{"namespace":"_","name":"iot"}'
 ```
 
+To force delete a package that contains entities, set the force parameter to true. Failure
+will return an error either for failure to delete an action within the package or the package itself.
+The package will not be attempted to be deleted until all actions are successfully deleted.
+
+```bash
+curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/packages/iot?force=true \
+-X DELETE
+```
+
 ## Activations
 
 To get the list of the last 3 activations use a HTTP request with a `GET` method, passing the query parameter `limit=3`
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/PackagesApiTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/PackagesApiTests.scala
index c52ba07..578e7ff 100644
--- a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/PackagesApiTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/PackagesApiTests.scala
@@ -797,7 +797,7 @@ class PackagesApiTests extends ControllerTestCommon with WhiskPackagesApi {
     }
   }
 
-  it should "reject delete non-empty package" in {
+  it should "delete package and its actions if force flag is set to true" in {
     implicit val tid = transid()
     val provider = WhiskPackage(namespace, aname())
     val action = WhiskAction(provider.namespace.addPath(provider.name), aname(), jsDefault("??"))
@@ -811,10 +811,32 @@ class PackagesApiTests extends ControllerTestCommon with WhiskPackagesApi {
       }
     }
 
+    Delete(s"$collectionPath/${provider.name}?force=true") ~> Route.seal(routes(creds)) ~> check {
+      status should be(OK)
+      val response = responseAs[WhiskPackage]
+      response should be(provider)
+    }
+  }
+
+  it should "reject delete non-empty package if force flag is not set" in {
+    implicit val tid = transid()
+    val provider = WhiskPackage(namespace, aname())
+    val action = WhiskAction(provider.namespace.addPath(provider.name), aname(), jsDefault("??"))
+    put(entityStore, provider)
+    put(entityStore, action)
+    org.apache.openwhisk.utils.retry {
+      Get(s"$collectionPath/${provider.name}") ~> Route.seal(routes(creds)) ~> check {
+        status should be(OK)
+        val response = responseAs[JsObject]
+        response.fields("actions").asInstanceOf[JsArray].elements.length should be(1)
+      }
+    }
+
+    val exceptionString = "Package not empty (contains 1 entity). Set force param or delete package contents."
     Delete(s"$collectionPath/${provider.name}") ~> Route.seal(routes(creds)) ~> check {
       status should be(Conflict)
       val response = responseAs[ErrorResponse]
-      response.error should include("Package not empty (contains 1 entity)")
+      response.error should include(exceptionString)
       response.code.id should not be empty
     }
   }