You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by cs...@apache.org on 2017/10/18 17:54:04 UTC
[incubator-openwhisk] branch master updated: Allow CLI to Save Code
from Action (#2544)
This is an automated email from the ASF dual-hosted git repository.
csantanapr 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 81668e1 Allow CLI to Save Code from Action (#2544)
81668e1 is described below
commit 81668e1f8b0a97c5f595c6aed36ead168b5c41de
Author: James Dubee <jw...@us.ibm.com>
AuthorDate: Wed Oct 18 13:54:01 2017 -0400
Allow CLI to Save Code from Action (#2544)
* Allow CLI to Save Code from Action
* Formatting changes
* Test refactor
* Update test
* Refactor
* Review updates
* Review updates
---
tests/src/test/scala/common/BaseWsk.scala | 4 +-
tests/src/test/scala/common/Wsk.scala | 13 +-
.../whisk/core/cli/test/WskBasicUsageTests.scala | 81 ++++++++-
tools/cli/go-whisk-cli/commands/action.go | 187 +++++++++++++++++----
tools/cli/go-whisk-cli/commands/flags.go | 15 +-
tools/cli/go-whisk-cli/commands/util.go | 135 ++++++++++++---
.../go-whisk-cli/wski18n/resources/en_US.all.json | 47 +++++-
tools/cli/go-whisk/whisk/action.go | 1 +
8 files changed, 410 insertions(+), 73 deletions(-)
diff --git a/tests/src/test/scala/common/BaseWsk.scala b/tests/src/test/scala/common/BaseWsk.scala
index 701e7ba..9d7cafe 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 65149be..50a501f 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 c635959..85e9fc8 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 cd692c0..05febd8 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 834c04a..e41d6b4 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 1cdb8b9..063c7db 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 3236830..acc6e2c 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 7284ae6..36d4680 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 {
--
To stop receiving notification emails like this one, please contact
['"commits@openwhisk.apache.org" <co...@openwhisk.apache.org>'].