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 <jwdu...@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" <commits@openwhisk.apache.org>'].