You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by be...@apache.org on 2017/07/12 17:20:42 UTC

[incubator-openwhisk] branch master updated: Get Action URL from CLI (#2461)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new b5b47d8  Get Action URL from CLI (#2461)
b5b47d8 is described below

commit b5b47d81638a821e930301628ca38afe909b7d71
Author: James Dubee <jw...@us.ibm.com>
AuthorDate: Wed Jul 12 13:20:40 2017 -0400

    Get Action URL from CLI (#2461)
---
 tests/src/test/scala/common/Wsk.scala              |  6 ++-
 .../whisk/core/cli/test/WskBasicUsageTests.scala   | 58 ++++++++++++++++++++++
 tools/cli/go-whisk-cli/commands/action.go          | 58 +++++++++++++---------
 tools/cli/go-whisk-cli/commands/flags.go           |  1 +
 tools/cli/go-whisk-cli/commands/util.go            | 19 +------
 .../go-whisk-cli/wski18n/resources/en_US.all.json  |  4 ++
 tools/cli/go-whisk/whisk/action.go                 | 49 ++++++++++++++++++
 tools/cli/go-whisk/whisk/shared.go                 | 21 +++++++-
 8 files changed, 171 insertions(+), 45 deletions(-)

diff --git a/tests/src/test/scala/common/Wsk.scala b/tests/src/test/scala/common/Wsk.scala
index df632db..70d2066 100644
--- a/tests/src/test/scala/common/Wsk.scala
+++ b/tests/src/test/scala/common/Wsk.scala
@@ -168,12 +168,14 @@ trait ListOrGetFromCollection extends FullyQualifiedNames {
         name: String,
         expectedExitCode: Int = SUCCESS_EXIT,
         summary: Boolean = false,
-        fieldFilter: Option[String] = None)(
+        fieldFilter: Option[String] = None,
+        url: Option[Boolean] = None)(
             implicit wp: WskProps): RunResult = {
         val params = Seq(noun, "get", "--auth", wp.authKey) ++
             Seq(fqn(name)) ++
             { if (summary) Seq("--summary") else Seq() } ++
-            { fieldFilter map { f => Seq(f) } getOrElse Seq() }
+            { fieldFilter map { f => Seq(f) } getOrElse Seq() } ++
+            { url map { u => Seq("--url") } getOrElse Seq() }
 
         cli(wp.overrides ++ params, expectedExitCode)
     }
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
index a5cf39c..6e06f71 100644
--- a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
+++ b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
@@ -21,6 +21,7 @@ import java.io.File
 import java.io.BufferedWriter
 import java.io.FileWriter
 import java.time.Instant
+import java.net.URLEncoder
 
 import scala.language.postfixOps
 import scala.concurrent.duration.Duration
@@ -695,6 +696,63 @@ class WskBasicUsageTests
             }
     }
 
+    it should "get an action URL" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val actionName = "action name@_-."
+            val packageName = "package name@_-."
+            val defaultPackageName = "default"
+            val webActionName = "web action name@_-."
+            val nonExistentActionName = "non-existence action"
+            val packagedAction = s"$packageName/$actionName"
+            val packagedWebAction = s"$packageName/$webActionName"
+            val (user, namespace) = WskAdmin.getUser(wskprops.authKey)
+            val encodedActionName = URLEncoder.encode(actionName, "UTF-8").replace("+", "%20")
+            val encodedPackageName = URLEncoder.encode(packageName, "UTF-8").replace("+", "%20")
+            val encodedWebActionName = URLEncoder.encode(webActionName, "UTF-8").replace("+", "%20")
+            val encodedNamespace = URLEncoder.encode(namespace, "UTF-8").replace("+", "%20")
+            val actionPath = "https://%s/api/%s/namespaces/%s/actions/%s"
+            val packagedActionPath = s"$actionPath/%s"
+            val webActionPath = "https://%s/api/%s/web/%s/%s/%s"
+
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, _) => action.create(actionName, defaultAction)
+            }
+
+            assetHelper.withCleaner(wsk.action, webActionName) {
+                (action, _) => action.create(webActionName, defaultAction, web = Some("true"))
+            }
+
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, _) => pkg.create(packageName)
+            }
+
+            assetHelper.withCleaner(wsk.action, packagedAction) {
+                (action, _) => action.create(packagedAction, defaultAction)
+            }
+
+            assetHelper.withCleaner(wsk.action, packagedWebAction) {
+                (action, _) => action.create(packagedWebAction, defaultAction, web = Some("true"))
+            }
+
+            wsk.action.get(actionName, url = Some(true)).
+                stdout should include(actionPath.format(wskprops.apihost, wskprops.apiversion, encodedNamespace, encodedActionName))
+
+            // Ensure url flag works when a field filter and summary flag are specified
+            wsk.action.get(actionName, url = Some(true), fieldFilter = Some("field"), summary = true).
+                stdout should include(actionPath.format(wskprops.apihost, wskprops.apiversion, encodedNamespace, encodedActionName))
+
+            wsk.action.get(webActionName, url = Some(true)).
+                stdout should include(webActionPath.format(wskprops.apihost, wskprops.apiversion, encodedNamespace, defaultPackageName, encodedWebActionName))
+
+            wsk.action.get(packagedAction, url = Some(true)).
+                stdout should include(packagedActionPath.format(wskprops.apihost, wskprops.apiversion, encodedNamespace, encodedPackageName, encodedActionName))
+
+            wsk.action.get(packagedWebAction, url = Some(true)).
+                stdout should include(webActionPath.format(wskprops.apihost, wskprops.apiversion, encodedNamespace, encodedPackageName, encodedWebActionName))
+
+            wsk.action.get(nonExistentActionName, url = Some(true), expectedExitCode = NOT_FOUND)
+    }
+
     behavior of "Wsk packages"
 
     it should "create, and delete a package" in {
diff --git a/tools/cli/go-whisk-cli/commands/action.go b/tools/cli/go-whisk-cli/commands/action.go
index 20a7926..97e361f 100644
--- a/tools/cli/go-whisk-cli/commands/action.go
+++ b/tools/cli/go-whisk-cli/commands/action.go
@@ -33,13 +33,13 @@ import (
     "github.com/mattn/go-colorable"
 )
 
-const MEMORY_LIMIT = 256
-const TIMEOUT_LIMIT = 60000
-const LOGSIZE_LIMIT = 10
-const ACTIVATION_ID = "activationId"
-const WEB_EXPORT_ANNOT = "web-export"
-const RAW_HTTP_ANNOT = "raw-http"
-const FINAL_ANNOT = "final"
+const MEMORY_LIMIT      = 256
+const TIMEOUT_LIMIT     = 60000
+const LOGSIZE_LIMIT     = 10
+const ACTIVATION_ID     = "activationId"
+const WEB_EXPORT_ANNOT  = "web-export"
+const RAW_HTTP_ANNOT    = "raw-http"
+const FINAL_ANNOT       = "final"
 
 var actionCmd = &cobra.Command{
     Use:   "action",
@@ -195,7 +195,7 @@ func handleInvocationResponse(
 }
 
 var actionGetCmd = &cobra.Command{
-    Use:           "get ACTION_NAME [FIELD_FILTER]",
+    Use:           "get ACTION_NAME [FIELD_FILTER | --summary | --url]",
     Short:         wski18n.T("get action"),
     SilenceUsage:  true,
     SilenceErrors: true,
@@ -210,7 +210,7 @@ var actionGetCmd = &cobra.Command{
             return whiskErr
         }
 
-        if len(args) > 1 {
+        if !flags.action.url && !flags.common.summary && len(args) > 1 {
             field = args[1]
 
             if !fieldExists(&whisk.Action{}, field) {
@@ -228,7 +228,13 @@ var actionGetCmd = &cobra.Command{
             return actionGetError(qualifiedName.entityName, err)
         }
 
-        if flags.common.summary {
+        if flags.action.url {
+            actionURL := action.ActionURL(Properties.APIHost,
+                DefaultOpenWhiskApiPath,
+                Properties.APIVersion,
+                qualifiedName.packageName)
+            printActionGetWithURL(qualifiedName.entity, actionURL)
+        } else if flags.common.summary {
             printSummary(action)
         } else {
             if len(field) > 0 {
@@ -822,6 +828,17 @@ func printActionGetWithField(entityName string, field string, action *whisk.Acti
     printField(action, field)
 }
 
+func printActionGetWithURL(entityName string, actionURL string) {
+    fmt.Fprintf(
+        color.Output,
+        wski18n.T("{{.ok}} got action {{.name}}\n",
+            map[string]interface{}{
+                "ok": color.GreenString("ok:"),
+                "name": boldString(entityName),
+            }))
+    fmt.Println(actionURL)
+}
+
 func printActionGet(entityName string, action *whisk.Action) {
     fmt.Fprintf(
         color.Output,
@@ -846,7 +863,7 @@ func printActionDeleted(entityName string) {
 }
 
 // Check if the specified action is a web-action
-func isWebAction(client *whisk.Client, qname QualifiedName) error {
+func isWebAction(client *whisk.Client, qname QualifiedName) (error) {
     var err error = nil
 
     savedNs := client.Namespace
@@ -854,6 +871,7 @@ func isWebAction(client *whisk.Client, qname QualifiedName) error {
     fullActionName := "/" + qname.namespace + "/" + qname.entityName
 
     action, _, err := client.Actions.Get(qname.entityName)
+
     if err != nil {
         whisk.Debug(whisk.DbgError, "client.Actions.Get(%s) error: %s\n", fullActionName, err)
         whisk.Debug(whisk.DbgError, "Unable to obtain action '%s' for web action validation\n", fullActionName)
@@ -864,23 +882,14 @@ func isWebAction(client *whisk.Client, qname QualifiedName) error {
     } else {
         err = errors.New(wski18n.T("Action '{{.name}}' is not a web action. Issue 'wsk action update {{.name}} --web true' to convert the action to a web action.",
             map[string]interface{}{"name": fullActionName}))
-        weVal := getValue(action.Annotations, "web-export")
-        if (weVal == nil) {
-            whisk.Debug(whisk.DbgError, "getValue(annotations, web-export) for action %s found no value\n", fullActionName)
-        } else {
-            var webExport bool
-            var ok bool
-            if webExport, ok = weVal.(bool); !ok {
-                whisk.Debug(whisk.DbgError, "web-export annotation value (%v) is not a boolean\n", weVal)
-            } else if !webExport {
-                whisk.Debug(whisk.DbgError, "web-export annotation value is false\n", weVal)
-            } else {
-                err = nil
-            }
+
+        if action.WebAction() {
+            err = nil
         }
     }
 
     client.Namespace = savedNs
+
     return err
 }
 
@@ -921,6 +930,7 @@ func init() {
     actionInvokeCmd.Flags().BoolVarP(&flags.action.result, "result", "r", false, wski18n.T("blocking invoke; show only activation result (unless there is a failure)"))
 
     actionGetCmd.Flags().BoolVarP(&flags.common.summary, "summary", "s", false, wski18n.T("summarize action details"))
+    actionGetCmd.Flags().BoolVarP(&flags.action.url, "url", "r", false, wski18n.T("get action url"))
 
     actionListCmd.Flags().IntVarP(&flags.common.skip, "skip", "s", 0, wski18n.T("exclude the first `SKIP` number of actions from the result"))
     actionListCmd.Flags().IntVarP(&flags.common.limit, "limit", "l", 30, wski18n.T("only return `LIMIT` number of actions from the collection"))
diff --git a/tools/cli/go-whisk-cli/commands/flags.go b/tools/cli/go-whisk-cli/commands/flags.go
index 19c9cbc..dd0645b 100644
--- a/tools/cli/go-whisk-cli/commands/flags.go
+++ b/tools/cli/go-whisk-cli/commands/flags.go
@@ -126,6 +126,7 @@ type ActionFlags struct {
     result      bool
     kind        string
     main        string
+    url         bool
 }
 
 func IsVerbose() bool {
diff --git a/tools/cli/go-whisk-cli/commands/util.go b/tools/cli/go-whisk-cli/commands/util.go
index 1871727..d8cea19 100644
--- a/tools/cli/go-whisk-cli/commands/util.go
+++ b/tools/cli/go-whisk-cli/commands/util.go
@@ -487,26 +487,11 @@ func getKeys(keyValueArr whisk.KeyValueArr) ([]string) {
     return res
 }
 
-func getValue(keyValueArr whisk.KeyValueArr, key string) (interface{}) {
-    var res interface{}
-
-    for i := 0; i < len(keyValueArr); i++ {
-        if keyValueArr[i].Key == key {
-            res = keyValueArr[i].Value
-            break;
-        }
-    }
-
-    whisk.Debug(whisk.DbgInfo, "Got value '%v' from '%v' for key '%s'\n", res, keyValueArr, key)
-
-    return res
-}
-
 func getValueString(keyValueArr whisk.KeyValueArr, key string) (string) {
     var value interface{}
     var res string
 
-    value = getValue(keyValueArr, key)
+    value = keyValueArr.GetValue(key)
     castedValue, canCast := value.(string)
 
     if (canCast) {
@@ -522,7 +507,7 @@ func getChildValues(keyValueArr whisk.KeyValueArr, key string, childKey string)
     var value interface{}
     var res []interface{}
 
-    value = getValue(keyValueArr, key)
+    value = keyValueArr.GetValue(key)
 
     castedValue, canCast := value.([]interface{})
     if canCast {
diff --git a/tools/cli/go-whisk-cli/wski18n/resources/en_US.all.json b/tools/cli/go-whisk-cli/wski18n/resources/en_US.all.json
index ec409ad..bcc776d 100644
--- a/tools/cli/go-whisk-cli/wski18n/resources/en_US.all.json
+++ b/tools/cli/go-whisk-cli/wski18n/resources/en_US.all.json
@@ -1466,5 +1466,9 @@
   {
     "id": "An entity name, '{{.name}}', was provided instead of a namespace. Valid namespaces are of the following format: /NAMESPACE.",
     "translation": "An entity name, '{{.name}}', was provided instead of a namespace. Valid namespaces are of the following format: /NAMESPACE."
+  },
+  {
+    "id": "get action url",
+    "translation": "get action url"
   }
 ]
diff --git a/tools/cli/go-whisk/whisk/action.go b/tools/cli/go-whisk/whisk/action.go
index 101f6d8..d370acf 100644
--- a/tools/cli/go-whisk/whisk/action.go
+++ b/tools/cli/go-whisk/whisk/action.go
@@ -22,6 +22,8 @@ import (
     "net/http"
     "errors"
     "net/url"
+    "strings"
+
     "../wski18n"
 )
 
@@ -57,6 +59,53 @@ type ActionListOptions struct {
     Docs        bool        `url:"docs,omitempty"`
 }
 
+/*
+Determines if an action is a web action by examining the action's annotations. A value of true is returned if the
+action's annotations contains a "web-export" key and its associated value is a boolean value of "true". Otherwise, false
+is returned.
+ */
+func (action Action) WebAction() (webExportValue bool) {
+    webExport := action.Annotations.GetValue("web-export")
+    webExportValue, _ = webExport.(bool)
+
+    Debug(DbgInfo, "Web export value is '%t'\n", webExportValue)
+
+    return webExportValue
+}
+
+/*
+Returns the URL of an action as a string. A valid API host, path and version must be passed. A package that contains the
+action must be passed as well. An empty string must be passed if the action is not packaged.
+ */
+func (action Action) ActionURL(apiHost string, apiPath string, apiVersion string, pkg string) (actionURL string) {
+    webActionPath := "https://%s%s/%s/web/%s/%s/%s"
+    actionPath := "https://%s%s/%s/namespaces/%s/actions/%s"
+    packagedActionPath := actionPath + "/%s"
+    namespace := strings.Split(action.Namespace, "/")[0]
+    namespace = strings.Replace(url.QueryEscape(namespace), "+", "%20", -1)
+    name := strings.Replace(url.QueryEscape(action.Name), "+", "%20", -1)
+    pkg = strings.Replace(url.QueryEscape(pkg), "+", "%20", -1)
+
+    if action.WebAction() {
+        if len(pkg) == 0 {
+            pkg = "default"
+        }
+
+        actionURL = fmt.Sprintf(webActionPath, apiHost, apiPath, apiVersion, namespace, pkg, name)
+        Debug(DbgInfo, "Web action URL: %s\n", actionURL)
+    } else {
+        if len(pkg) == 0 {
+            actionURL = fmt.Sprintf(actionPath, apiHost, apiPath, apiVersion, namespace, name)
+            Debug(DbgInfo, "Packaged action URL: %s\n", actionURL)
+        } else {
+            actionURL = fmt.Sprintf(packagedActionPath, apiHost, apiPath, apiVersion, namespace, pkg, name)
+            Debug(DbgInfo, "Action URL: %s\n", actionURL)
+        }
+    }
+
+    return actionURL
+}
+
 ////////////////////
 // Action Methods //
 ////////////////////
diff --git a/tools/cli/go-whisk/whisk/shared.go b/tools/cli/go-whisk/whisk/shared.go
index a39722f..f91787d 100644
--- a/tools/cli/go-whisk/whisk/shared.go
+++ b/tools/cli/go-whisk/whisk/shared.go
@@ -20,12 +20,29 @@ package whisk
 import "encoding/json"
 
 type KeyValue struct {
-    Key  string         `json:"key"`
-    Value interface{}   `json:"value"`
+    Key     string          `json:"key"`
+    Value   interface{}     `json:"value"`
 }
 
 type KeyValueArr []KeyValue
 
+/*
+Retrieves a value associated with a given key from a KeyValueArr. A key of type string must be passed to the method.
+An interface will be returned containing the found value. If a key could not be found, a nil value will be returned.
+ */
+func (keyValueArr KeyValueArr) GetValue(key string) (res interface{}) {
+    for i := 0; i < len(keyValueArr); i++ {
+        if keyValueArr[i].Key == key {
+            res = keyValueArr[i].Value
+            break;
+        }
+    }
+
+    Debug(DbgInfo, "Got value '%v' for key '%s' from '%v'\n", res, key, keyValueArr)
+
+    return res
+}
+
 type Annotations []map[string]interface{}
 
 type Parameters *json.RawMessage

-- 
To stop receiving notification emails like this one, please contact
['"commits@openwhisk.apache.org" <co...@openwhisk.apache.org>'].