You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2017/10/18 17:54:06 UTC

[GitHub] csantanapr closed pull request #2544: Allow CLI to Save Code from Action

csantanapr closed pull request #2544: Allow CLI to Save Code from Action
URL: https://github.com/apache/incubator-openwhisk/pull/2544
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/tests/src/test/scala/common/BaseWsk.scala b/tests/src/test/scala/common/BaseWsk.scala
index 701e7baf32..9d7cafe980 100644
--- a/tests/src/test/scala/common/BaseWsk.scala
+++ b/tests/src/test/scala/common/BaseWsk.scala
@@ -160,7 +160,9 @@ trait BaseListOrGetFromCollection extends FullyQualifiedNames {
           expectedExitCode: Int = SUCCESS_EXIT,
           summary: Boolean = false,
           fieldFilter: Option[String] = None,
-          url: Option[Boolean] = None)(implicit wp: WskProps): RunResult
+          url: Option[Boolean] = None,
+          save: Option[Boolean] = None,
+          saveAs: Option[String] = None)(implicit wp: WskProps): RunResult
 }
 
 trait BaseDeleteFromCollection extends FullyQualifiedNames {
diff --git a/tests/src/test/scala/common/Wsk.scala b/tests/src/test/scala/common/Wsk.scala
index 65149be4ff..50a501f26b 100644
--- a/tests/src/test/scala/common/Wsk.scala
+++ b/tests/src/test/scala/common/Wsk.scala
@@ -109,7 +109,10 @@ trait ListOrGetFromCollectionCLI extends BaseListOrGetFromCollection {
                    expectedExitCode: Int = SUCCESS_EXIT,
                    summary: Boolean = false,
                    fieldFilter: Option[String] = None,
-                   url: Option[Boolean] = None)(implicit wp: WskProps): RunResult = {
+                   url: Option[Boolean] = None,
+                   save: Option[Boolean] = None,
+                   saveAs: Option[String] = 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 =>
@@ -119,6 +122,14 @@ trait ListOrGetFromCollectionCLI extends BaseListOrGetFromCollection {
       url map { u =>
         Seq("--url")
       } getOrElse Seq()
+    } ++ {
+      save map { s =>
+        Seq("--save")
+      } getOrElse Seq()
+    } ++ {
+      saveAs map { s =>
+        Seq("--save-as", s)
+      } 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 c63595917a..85e9fc88d8 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.time.Instant
 import java.net.URLEncoder
 import java.nio.charset.StandardCharsets
 import java.time.Clock
+import java.io.File
 
 import scala.language.postfixOps
 import scala.concurrent.duration.Duration
@@ -142,8 +143,9 @@ class WskBasicUsageTests extends TestHelpers with WskTestHelpers {
   }
 
   it should "reject create with missing file" in {
-    wsk.action.create("missingFile", Some("notfound"), expectedExitCode = MISUSE_EXIT).stderr should include(
-      "not a valid file")
+    val name = "notfound"
+    wsk.action.create("missingFile", Some(name), expectedExitCode = MISUSE_EXIT).stderr should include(
+      s"File '$name' is not a valid file or it does not exist")
   }
 
   it should "reject action update when specified file is missing" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
@@ -811,6 +813,81 @@ class WskBasicUsageTests extends TestHelpers with WskTestHelpers {
     stdoutNoDescOrParams should include regex (s"(?i)action /${namespace}/${actNameNoDescOrParams}\\s*\\(parameters: none defined\\)")
   }
 
+  it should "save action code to file" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
+    val name = "saveAction"
+    val seqName = "seqName"
+    val dockerName = "dockerName"
+    val containerName = s"bogus${Random.alphanumeric.take(16).mkString.toLowerCase}"
+    val saveName = s"save-as-$name.js"
+    val badSaveName = s"bad-directory${File.separator}$saveName"
+
+    // Test for successful --save
+    assetHelper.withCleaner(wsk.action, name) { (action, _) =>
+      action.create(name, defaultAction)
+    }
+
+    val saveMsg = wsk.action.get(name, save = Some(true)).stdout
+
+    saveMsg should include(s"saved action code to ")
+
+    val savePath = saveMsg.split("ok: saved action code to ")(1).trim()
+    val saveFile = new File(savePath);
+
+    try {
+      saveFile.exists shouldBe true
+
+      // Test for failure saving file when it already exist
+      wsk.action.get(name, save = Some(true), expectedExitCode = MISUSE_EXIT).stderr should include(
+        s"The file '$name.js' already exists")
+    } finally {
+      saveFile.delete()
+    }
+
+    // Test for successful --save-as
+    val saveAsMsg = wsk.action.get(name, saveAs = Some(saveName)).stdout
+
+    saveAsMsg should include(s"saved action code to ")
+
+    val saveAsPath = saveAsMsg.split("ok: saved action code to ")(1).trim()
+    val saveAsFile = new File(saveAsPath);
+
+    try {
+      saveAsFile.exists shouldBe true
+
+      // Test for failure saving file when it already exist
+      wsk.action.get(name, saveAs = Some(saveName), expectedExitCode = MISUSE_EXIT).stderr should include(
+        s"The file '$saveName' already exists")
+    } finally {
+      saveAsFile.delete()
+    }
+
+    // Test for failure when using an invalid filename
+    wsk.action.get(name, saveAs = Some(badSaveName), expectedExitCode = MISUSE_EXIT).stderr should include(
+      s"Cannot create file '$badSaveName'")
+
+    // Test for failure saving Docker images
+    assetHelper.withCleaner(wsk.action, dockerName) { (action, _) =>
+      action.create(dockerName, None, docker = Some(containerName))
+    }
+
+    wsk.action.get(dockerName, save = Some(true), expectedExitCode = MISUSE_EXIT).stderr should include(
+      "Cannot save Docker images")
+
+    wsk.action.get(dockerName, saveAs = Some(dockerName), expectedExitCode = MISUSE_EXIT).stderr should include(
+      "Cannot save Docker images")
+
+    // Tes for failure saving sequences
+    assetHelper.withCleaner(wsk.action, seqName) { (action, _) =>
+      action.create(seqName, Some(name), kind = Some("sequence"))
+    }
+
+    wsk.action.get(seqName, save = Some(true), expectedExitCode = MISUSE_EXIT).stderr should include(
+      "Cannot save action sequences")
+
+    wsk.action.get(seqName, saveAs = Some(seqName), expectedExitCode = MISUSE_EXIT).stderr should include(
+      "Cannot save action sequences")
+  }
+
   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 cd692c0240..05febd85d1 100644
--- a/tools/cli/go-whisk-cli/commands/action.go
+++ b/tools/cli/go-whisk-cli/commands/action.go
@@ -24,6 +24,7 @@ import (
     "path/filepath"
     "io"
     "strings"
+    "os"
 
     "../../go-whisk/whisk"
     "../wski18n"
@@ -33,13 +34,29 @@ 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
+    TIMEOUT_LIMIT     = 60000
+    LOGSIZE_LIMIT     = 10
+    ACTIVATION_ID     = "activationId"
+    WEB_EXPORT_ANNOT  = "web-export"
+    RAW_HTTP_ANNOT    = "raw-http"
+    FINAL_ANNOT       = "final"
+    NODE_JS_EXT       = ".js"
+    PYTHON_EXT        = ".py"
+    JAVA_EXT          = ".jar"
+    SWIFT_EXT         = ".swift"
+    ZIP_EXT           = ".zip"
+    PHP_EXT           = ".php"
+    NODE_JS           = "nodejs"
+    PYTHON            = "python"
+    JAVA              = "java"
+    SWIFT             = "swift"
+    PHP               = "php"
+    DEFAULT           = "default"
+    BLACKBOX          = "blackbox"
+    SEQUENCE          = "sequence"
+)
 
 var actionCmd = &cobra.Command{
     Use:   "action",
@@ -242,6 +259,8 @@ var actionGetCmd = &cobra.Command{
             printActionGetWithURL(qualifiedName.GetEntity(), actionURL)
         } else if flags.common.summary {
             printSummary(action)
+        } else if cmd.LocalFlags().Changed(SAVE_AS_FLAG) || cmd.LocalFlags().Changed(SAVE_FLAG) {
+            return saveCode(*action, flags.action.saveAs)
         } else {
             if len(field) > 0 {
                 printActionGetWithField(qualifiedName.GetEntityName(), field, action)
@@ -398,7 +417,7 @@ func parseAction(cmd *cobra.Command, args []string, update bool) (*whisk.Action,
     } else if flags.action.sequence {
         if len(args) == 2 {
             action.Exec = new(whisk.Exec)
-            action.Exec.Kind = "sequence"
+            action.Exec.Kind = SEQUENCE
             action.Exec.Components = csvToQualifiedActions(args[1])
         } else {
             return nil, noArtifactError()
@@ -444,8 +463,7 @@ func getExec(args []string, params ActionFlags) (*whisk.Exec, error) {
             return nil, err
         }
 
-        if ext == ".zip" || ext == ".jar" {
-            // Base64 encode the file
+        if ext == ZIP_EXT || ext == JAVA_EXT {
             code = base64.StdEncoding.EncodeToString([]byte(code))
         }
 
@@ -459,24 +477,24 @@ func getExec(args []string, params ActionFlags) (*whisk.Exec, error) {
     if len(kind) > 0 {
         exec.Kind = kind
     } else if len(docker) > 0 || isNative {
-        exec.Kind = "blackbox"
+        exec.Kind = BLACKBOX
         if isNative {
             exec.Image = "openwhisk/dockerskeleton"
         } else {
             exec.Image = docker
         }
-    } else if ext == ".swift" {
-        exec.Kind = "swift:default"
-    } else if ext == ".js" {
-        exec.Kind = "nodejs:default"
-    } else if ext == ".py" {
-        exec.Kind = "python:default"
-    } else if ext == ".jar" {
-        exec.Kind = "java:default"
-    } else if ext == ".php" {
-        exec.Kind = "php:default"
+    } else if ext == SWIFT_EXT {
+        exec.Kind = fmt.Sprintf("%s:%s", SWIFT, DEFAULT)
+    } else if ext == NODE_JS_EXT {
+        exec.Kind = fmt.Sprintf("%s:%s", NODE_JS, DEFAULT)
+    } else if ext == PYTHON_EXT {
+        exec.Kind = fmt.Sprintf("%s:%s", PYTHON, DEFAULT)
+    } else if ext == JAVA_EXT {
+        exec.Kind = fmt.Sprintf("%s:%s", JAVA, DEFAULT)
+    } else if ext == PHP_EXT {
+        exec.Kind = fmt.Sprintf("%s:%s", PHP, DEFAULT)
     } else {
-        if ext == ".zip" {
+        if ext == ZIP_EXT {
             return nil, zipKindError()
         } else {
             return nil, extensionError(ext)
@@ -495,6 +513,86 @@ func getExec(args []string, params ActionFlags) (*whisk.Exec, error) {
     return exec, nil
 }
 
+func getBinaryKindExtension(runtime string) (extension string) {
+    switch strings.ToLower(runtime) {
+    case JAVA:
+        extension = JAVA_EXT
+    default:
+        extension = ZIP_EXT
+    }
+
+    return extension
+}
+
+func getKindExtension(runtime string) (extension string) {
+    switch strings.ToLower(runtime) {
+    case NODE_JS:
+        extension = NODE_JS_EXT
+    case PYTHON:
+        extension = PYTHON_EXT
+    case SWIFT:
+        fallthrough
+    case PHP:
+        extension = fmt.Sprintf(".%s", runtime)
+    }
+
+    return extension
+}
+
+func saveCode(action whisk.Action, filename string) (err error) {
+    var code string
+    var runtime string
+    var exec whisk.Exec
+
+    exec = *action.Exec
+    runtime = strings.Split(exec.Kind, ":")[0]
+
+    if strings.ToLower(runtime) == BLACKBOX {
+        return cannotSaveImageError()
+    } else if strings.ToLower(runtime) == SEQUENCE {
+        return cannotSaveSequenceError()
+    }
+
+    if exec.Code != nil {
+        code = *exec.Code
+    }
+
+    if *exec.Binary {
+        decoded, _ := base64.StdEncoding.DecodeString(code)
+        code = string(decoded)
+
+        if len(filename) == 0 {
+            filename = action.Name + getBinaryKindExtension(runtime)
+        }
+    } else {
+        if len(filename) == 0 {
+            filename = action.Name + getKindExtension(runtime)
+        }
+    }
+
+    if exists, err := FileExists(filename); err != nil {
+        return err
+    } else if exists {
+        return fileExistsError(filename)
+    }
+
+    if err := writeFile(filename, code); err != nil {
+        return err
+    }
+
+    pwd, err := os.Getwd()
+    if err != nil {
+        whisk.Debug(whisk.DbgError, "os.Getwd() error: %s\n", err)
+        return err
+    }
+
+    savedPath := fmt.Sprintf("%s%s%s", pwd, string(os.PathSeparator), filename)
+
+    printSavedActionCodeSuccess(savedPath)
+
+    return nil
+}
+
 func webAction(webMode string, annotations whisk.KeyValueArr, entityName string, preserveAnnotations bool) (whisk.KeyValueArr, error){
     switch strings.ToLower(webMode) {
     case "yes":
@@ -769,6 +867,22 @@ func javaEntryError() (error) {
     return nonNestedError(errMsg)
 }
 
+func cannotSaveImageError() (error) {
+    return nonNestedError(wski18n.T("Cannot save Docker images"))
+}
+
+func cannotSaveSequenceError() (error) {
+    return nonNestedError(wski18n.T("Cannot save action sequences"))
+}
+
+func fileExistsError(file string) (error) {
+    errMsg := wski18n.T("The file '{{.file}}' already exists", map[string]interface{} {
+        "file": file,
+    })
+
+    return nonNestedError(errMsg)
+}
+
 func printActionCreated(entityName string) {
     fmt.Fprintf(
         color.Output,
@@ -876,6 +990,17 @@ func printActionDeleted(entityName string) {
             }))
 }
 
+func printSavedActionCodeSuccess(name string) {
+    fmt.Fprintf(
+        color.Output,
+        wski18n.T(
+            "{{.ok}} saved action code to {{.name}}\n",
+            map[string]interface{}{
+                "ok": color.GreenString("ok:"),
+                "name": boldString(name),
+            }))
+}
+
 // Check if the specified action is a web-action
 func isWebAction(client *whisk.Client, qname QualifiedName) (error) {
     var err error = nil
@@ -914,14 +1039,14 @@ func init() {
     actionCreateCmd.Flags().BoolVar(&flags.action.sequence, "sequence", false, wski18n.T("treat ACTION as comma separated sequence of actions to invoke"))
     actionCreateCmd.Flags().StringVar(&flags.action.kind, "kind", "", wski18n.T("the `KIND` of the action runtime (example: swift:default, nodejs:default)"))
     actionCreateCmd.Flags().StringVar(&flags.action.main, "main", "", wski18n.T("the name of the action entry point (function or fully-qualified method name when applicable)"))
-    actionCreateCmd.Flags().IntVarP(&flags.action.timeout, "timeout", "t", TIMEOUT_LIMIT, wski18n.T("the timeout `LIMIT` in milliseconds after which the action is terminated"))
-    actionCreateCmd.Flags().IntVarP(&flags.action.memory, "memory", "m", MEMORY_LIMIT, wski18n.T("the maximum memory `LIMIT` in MB for the action"))
-    actionCreateCmd.Flags().IntVarP(&flags.action.logsize, "logsize", "l", LOGSIZE_LIMIT, wski18n.T("the maximum log size `LIMIT` in MB for the action"))
+    actionCreateCmd.Flags().IntVarP(&flags.action.timeout, TIMEOUT_FLAG, "t", TIMEOUT_LIMIT, wski18n.T("the timeout `LIMIT` in milliseconds after which the action is terminated"))
+    actionCreateCmd.Flags().IntVarP(&flags.action.memory, MEMORY_FLAG, "m", MEMORY_LIMIT, wski18n.T("the maximum memory `LIMIT` in MB for the action"))
+    actionCreateCmd.Flags().IntVarP(&flags.action.logsize, LOG_SIZE_FLAG, "l", LOGSIZE_LIMIT, wski18n.T("the maximum log size `LIMIT` in MB for the action"))
     actionCreateCmd.Flags().StringSliceVarP(&flags.common.annotation, "annotation", "a", nil, wski18n.T("annotation values in `KEY VALUE` format"))
     actionCreateCmd.Flags().StringVarP(&flags.common.annotFile, "annotation-file", "A", "", wski18n.T("`FILE` containing annotation values in JSON format"))
     actionCreateCmd.Flags().StringSliceVarP(&flags.common.param, "param", "p", nil, wski18n.T("parameter values in `KEY VALUE` format"))
     actionCreateCmd.Flags().StringVarP(&flags.common.paramFile, "param-file", "P", "", wski18n.T("`FILE` containing parameter values in JSON format"))
-    actionCreateCmd.Flags().StringVar(&flags.action.web, "web", "", wski18n.T("treat ACTION as a web action, a raw HTTP web action, or as a standard action; yes | true = web action, raw = raw HTTP web action, no | false = standard action"))
+    actionCreateCmd.Flags().StringVar(&flags.action.web, WEB_FLAG, "", wski18n.T("treat ACTION as a web action, a raw HTTP web action, or as a standard action; yes | true = web action, raw = raw HTTP web action, no | false = standard action"))
 
     actionUpdateCmd.Flags().BoolVar(&flags.action.native, "native", false, wski18n.T("treat ACTION as native action (zip file provides a compatible executable to run)"))
     actionUpdateCmd.Flags().StringVar(&flags.action.docker, "docker", "", wski18n.T("use provided docker image (a path on DockerHub) to run the action"))
@@ -929,14 +1054,14 @@ func init() {
     actionUpdateCmd.Flags().BoolVar(&flags.action.sequence, "sequence", false, wski18n.T("treat ACTION as comma separated sequence of actions to invoke"))
     actionUpdateCmd.Flags().StringVar(&flags.action.kind, "kind", "", wski18n.T("the `KIND` of the action runtime (example: swift:default, nodejs:default)"))
     actionUpdateCmd.Flags().StringVar(&flags.action.main, "main", "", wski18n.T("the name of the action entry point (function or fully-qualified method name when applicable)"))
-    actionUpdateCmd.Flags().IntVarP(&flags.action.timeout, "timeout", "t", TIMEOUT_LIMIT, wski18n.T("the timeout `LIMIT` in milliseconds after which the action is terminated"))
-    actionUpdateCmd.Flags().IntVarP(&flags.action.memory, "memory", "m", MEMORY_LIMIT, wski18n.T("the maximum memory `LIMIT` in MB for the action"))
-    actionUpdateCmd.Flags().IntVarP(&flags.action.logsize, "logsize", "l", LOGSIZE_LIMIT, wski18n.T("the maximum log size `LIMIT` in MB for the action"))
+    actionUpdateCmd.Flags().IntVarP(&flags.action.timeout, TIMEOUT_FLAG, "t", TIMEOUT_LIMIT, wski18n.T("the timeout `LIMIT` in milliseconds after which the action is terminated"))
+    actionUpdateCmd.Flags().IntVarP(&flags.action.memory, MEMORY_FLAG, "m", MEMORY_LIMIT, wski18n.T("the maximum memory `LIMIT` in MB for the action"))
+    actionUpdateCmd.Flags().IntVarP(&flags.action.logsize, LOG_SIZE_FLAG, "l", LOGSIZE_LIMIT, wski18n.T("the maximum log size `LIMIT` in MB for the action"))
     actionUpdateCmd.Flags().StringSliceVarP(&flags.common.annotation, "annotation", "a", []string{}, wski18n.T("annotation values in `KEY VALUE` format"))
     actionUpdateCmd.Flags().StringVarP(&flags.common.annotFile, "annotation-file", "A", "", wski18n.T("`FILE` containing annotation values in JSON format"))
     actionUpdateCmd.Flags().StringSliceVarP(&flags.common.param, "param", "p", []string{}, wski18n.T("parameter values in `KEY VALUE` format"))
     actionUpdateCmd.Flags().StringVarP(&flags.common.paramFile, "param-file", "P", "", wski18n.T("`FILE` containing parameter values in JSON format"))
-    actionUpdateCmd.Flags().StringVar(&flags.action.web, "web", "", wski18n.T("treat ACTION as a web action, a raw HTTP web action, or as a standard action; yes | true = web action, raw = raw HTTP web action, no | false = standard action"))
+    actionUpdateCmd.Flags().StringVar(&flags.action.web, WEB_FLAG, "", wski18n.T("treat ACTION as a web action, a raw HTTP web action, or as a standard action; yes | true = web action, raw = raw HTTP web action, no | false = standard action"))
 
     actionInvokeCmd.Flags().StringSliceVarP(&flags.common.param, "param", "p", []string{}, wski18n.T("parameter values in `KEY VALUE` format"))
     actionInvokeCmd.Flags().StringVarP(&flags.common.paramFile, "param-file", "P", "", wski18n.T("`FILE` containing parameter values in JSON format"))
@@ -945,6 +1070,8 @@ func init() {
 
     actionGetCmd.Flags().BoolVarP(&flags.common.summary, "summary", "s", false, wski18n.T("summarize action details; parameters with prefix \"*\" are bound, \"**\" are bound and finalized"))
     actionGetCmd.Flags().BoolVarP(&flags.action.url, "url", "r", false, wski18n.T("get action url"))
+    actionGetCmd.Flags().StringVar(&flags.action.saveAs, SAVE_AS_FLAG, "", wski18n.T("file to save action code to"))
+    actionGetCmd.Flags().BoolVarP(&flags.action.save, SAVE_FLAG, "", false, wski18n.T("save action code to file corresponding with action name"))
 
     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 834c04a3d4..e41d6b44f2 100644
--- a/tools/cli/go-whisk-cli/commands/flags.go
+++ b/tools/cli/go-whisk-cli/commands/flags.go
@@ -25,10 +25,15 @@ import (
 // Flags //
 ///////////
 
-const MEMORY_FLAG   = "memory"
-const LOG_SIZE_FLAG = "logsize"
-const TIMEOUT_FLAG  = "timeout"
-const WEB_FLAG      = "web"
+const (
+    MEMORY_FLAG     = "memory"
+    LOG_SIZE_FLAG   = "logsize"
+    TIMEOUT_FLAG    = "timeout"
+    WEB_FLAG        = "web"
+    SAVE_FLAG       = "save"
+    SAVE_AS_FLAG    = "save-as"
+)
+
 
 var cliDebug = os.Getenv("WSK_CLI_DEBUG")  // Useful for tracing init() code
 
@@ -139,6 +144,8 @@ type ActionFlags struct {
     kind        string
     main        string
     url         bool
+    save        bool
+    saveAs     string
 }
 
 func IsVerbose() bool {
diff --git a/tools/cli/go-whisk-cli/commands/util.go b/tools/cli/go-whisk-cli/commands/util.go
index 1cdb8b9138..063c7db2b7 100644
--- a/tools/cli/go-whisk-cli/commands/util.go
+++ b/tools/cli/go-whisk-cli/commands/util.go
@@ -585,21 +585,37 @@ func printJsonNoColor(decoded interface{}, stream ...io.Writer) {
 }
 
 func unpackGzip(inpath string, outpath string) error {
-    // Make sure the target file does not exist
-    if _, err := os.Stat(outpath); err == nil {
-        whisk.Debug(whisk.DbgError, "os.Stat reports file '%s' exists\n", outpath)
+    var exists bool
+    var err error
+
+    exists, err = FileExists(outpath)
+
+    if err != nil {
+        return err
+    }
+
+    if exists {
         errStr := wski18n.T("The file '{{.name}}' already exists.  Delete it and retry.",
             map[string]interface{}{"name": outpath})
         werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
         return werr
     }
 
-    // Make sure the input file exists
-    if _, err := os.Stat(inpath); err != nil {
-        whisk.Debug(whisk.DbgError, "os.Stat reports file '%s' does not exist\n", inpath)
-        errStr := wski18n.T("The file '{{.name}}' does not exist.", map[string]interface{}{"name": inpath})
-        werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
-        return werr
+    exists, err = FileExists(inpath)
+
+    if err != nil {
+        return err
+    }
+
+    if !exists {
+        errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist",
+            map[string]interface{}{
+                "name": inpath,
+            })
+        whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_USAGE,
+            whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+
+        return whiskErr
     }
 
     unGzFile, err := os.Create(outpath)
@@ -644,14 +660,22 @@ func unpackGzip(inpath string, outpath string) error {
 }
 
 func unpackZip(inpath string) error {
-    // Make sure the input file exists
-    if _, err := os.Stat(inpath); err != nil {
-        whisk.Debug(whisk.DbgError, "os.Stat reports file '%s' does not exist\n", inpath)
-        errStr := wski18n.T("The file '{{.name}}' does not exist.", map[string]interface{}{"name": inpath})
-        werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
-        return werr
+    exists, err := FileExists(inpath)
+
+    if err != nil {
+        return err
     }
 
+    if !exists {
+        errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist",
+            map[string]interface{}{
+                "name": inpath,
+            })
+        whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_USAGE,
+            whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+
+        return whiskErr
+    }
     zipFileReader, err := zip.OpenReader(inpath)
     if err != nil {
         whisk.Debug(whisk.DbgError, "zip.OpenReader(%s) failed: %s\n", inpath, err)
@@ -711,13 +735,21 @@ func unpackZip(inpath string) error {
 }
 
 func unpackTar(inpath string) error {
+    exists, err := FileExists(inpath)
 
-    // Make sure the input file exists
-    if _, err := os.Stat(inpath); err != nil {
-        whisk.Debug(whisk.DbgError, "os.Stat reports file '%s' does not exist\n", inpath)
-        errStr := wski18n.T("The file '{{.name}}' does not exist.", map[string]interface{}{"name": inpath})
-        werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
-        return werr
+    if err != nil {
+        return err
+    }
+
+    if !exists {
+        errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist",
+            map[string]interface{}{
+                "name": inpath,
+            })
+        whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_USAGE,
+            whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+
+        return whiskErr
     }
 
     tarFileReader, err := os.Open(inpath)
@@ -827,11 +859,17 @@ func getClientNamespace() (string) {
 }
 
 func readFile(filename string) (string, error) {
-    _, err := os.Stat(filename)
+    exists, err := FileExists(filename)
+
     if err != nil {
-        whisk.Debug(whisk.DbgError, "os.Stat(%s) error: %s\n", filename, err)
-        errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist: {{.err}}",
-                map[string]interface{}{"name": filename, "err": err})
+        return "", err
+    }
+
+    if !exists {
+        errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist",
+            map[string]interface{}{
+                "name": filename,
+            })
         whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_USAGE,
             whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
 
@@ -841,7 +879,7 @@ func readFile(filename string) (string, error) {
     file, err := ioutil.ReadFile(filename)
     if err != nil {
         whisk.Debug(whisk.DbgError, "os.ioutil.ReadFile(%s) error: %s\n", filename, err)
-        errMsg := wski18n.T("Unable to read '{{.name}}': {{.err}}",
+        errMsg := wski18n.T("Unable to read the file '{{.name}}': {{.err}}",
                 map[string]interface{}{"name": filename, "err": err})
         whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
             whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
@@ -851,6 +889,51 @@ func readFile(filename string) (string, error) {
     return string(file), nil
 }
 
+func writeFile(filename string, content string) (error) {
+    file, err := os.Create(filename)
+
+    if err != nil {
+        whisk.Debug(whisk.DbgError, "os.Create(%s) error: %#v\n", filename, err)
+        errMsg := wski18n.T("Cannot create file '{{.name}}': {{.err}}",
+            map[string]interface{}{"name": filename, "err": err})
+        whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_USAGE, whisk.DISPLAY_MSG,
+            whisk.DISPLAY_USAGE)
+        return whiskErr
+    }
+
+    defer file.Close()
+
+    if _, err = file.WriteString(content); err != nil {
+        whisk.Debug(whisk.DbgError, "File.WriteString(%s) error: %#v\n", content, err)
+        errMsg := wski18n.T("Cannot create file '{{.name}}': {{.err}}",
+            map[string]interface{}{"name": filename, "err": err})
+        whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_USAGE, whisk.DISPLAY_MSG,
+            whisk.DISPLAY_USAGE)
+        return whiskErr
+    }
+
+    return nil
+}
+
+func FileExists(file string) (bool, error) {
+    _, err := os.Stat(file)
+
+    if err != nil {
+        if os.IsNotExist(err) == true {
+            return false, nil
+        } else {
+            whisk.Debug(whisk.DbgError, "os.Stat(%s) error: %#v\n", file, err)
+            errMsg := wski18n.T("Cannot access file '{{.name}}': {{.err}}",
+                map[string]interface{}{"name": file, "err": err})
+            whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_USAGE,
+                whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
+            return true, whiskErr
+        }
+    }
+
+    return true, nil
+}
+
 func fieldExists(value interface{}, field string) (bool) {
     element := reflect.ValueOf(value).Elem()
 
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 323683006a..acc6e2c075 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
@@ -732,8 +732,8 @@
     "translation": "shared"
   },
   {
-    "id": "The file '{{.name}}' does not exist.",
-    "translation": "The file '{{.name}}' does not exist."
+    "id": "File '{{.name}}' is not a valid file or it does not exist",
+    "translation": "File '{{.name}}' is not a valid file or it does not exist"
   },
   {
     "id": "Error creating unGzip file '{{.name}}': {{.err}}",
@@ -876,12 +876,8 @@
     "translation": "Unable to get action '{{.name}}': {{.err}}"
   },
   {
-    "id": "File '{{.name}}' is not a valid file or it does not exist: {{.err}}",
-    "translation": "File '{{.name}}' is not a valid file or it does not exist: {{.err}}"
-  },
-  {
-    "id": "Unable to read '{{.name}}': {{.err}}",
-    "translation": "Unable to read '{{.name}}': {{.err}}"
+    "id": "Unable to read the file '{{.name}}': {{.err}}",
+    "translation": "Unable to read the file '{{.name}}': {{.err}}"
   },
   {
     "id": "'{{.name}}' is not a supported action runtime",
@@ -1526,4 +1522,37 @@
   {
     "id": "sorts a list alphabetically by order of [BASE_PATH | API_NAME], API_PATH, then API_VERB; only applicable within the limit/skip returned entity block",
     "translation": "sorts a list alphabetically by order of [BASE_PATH | API_NAME], API_PATH, then API_VERB; only applicable within the limit/skip returned entity block"
-  }]
+  },
+  {
+    "id": "prints bash command completion script to stdout",
+    "translation": "prints bash command completion script to stdout"
+  },
+  {
+    "id": "save action code to file corresponding with action name",
+    "translation": "save action code to file corresponding with action name"
+  },
+  {
+    "id": "file to save action code to",
+    "translation": "file to save action code to"
+  },
+  {
+    "id": "The file '{{.file}}' already exists",
+    "translation": "The file '{{.file}}' already exists"
+  },
+  {
+    "id": "{{.ok}} saved action code to {{.name}}\n",
+    "translation": "{{.ok}} saved action code to {{.name}}\n"
+  },
+  {
+    "id": "Cannot save Docker images",
+    "translation": "Cannot save Docker images"
+  },
+  {
+    "id": "Cannot save action sequences",
+    "translation": "Cannot save action sequences"
+  },
+  {
+    "id": "Cannot create file '{{.name}}': {{.err}}",
+    "translation": "Cannot create file '{{.name}}': {{.err}}"
+  }
+]
diff --git a/tools/cli/go-whisk/whisk/action.go b/tools/cli/go-whisk/whisk/action.go
index 7284ae6370..36d4680d5a 100644
--- a/tools/cli/go-whisk/whisk/action.go
+++ b/tools/cli/go-whisk/whisk/action.go
@@ -51,6 +51,7 @@ type Exec struct {
     Init        string      `json:"init,omitempty"`
     Main        string      `json:"main,omitempty"`
     Components  []string    `json:"components,omitempty"`    // List of fully qualified actions
+    Binary      *bool       `json:"binary,omitempty"`
 }
 
 type ActionListOptions struct {


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services