From e0e0d9716ee2fbccbc1d0eb6d8906093debd974f Mon Sep 17 00:00:00 2001 From: Brandon Lee Underwood Date: Thu, 10 Aug 2017 21:47:07 -0400 Subject: [PATCH] Allow CLI to sort entities by name (#2326) - Created interfaces `Printables` and `Sortables` - Made Actions, Triggers, Packages, Rules, APIs into Printables and Sortables - Made Activations into Printables and Sortables, Sort currently undefined - Made alphabetic sorting default, sort by last update time with --time flag - Changed sorting default back to last update time, --sort flag for alphabetical sorting - Updated flag name to "--name-sort"/"-n" - Updated Docs - Fixed rule status printing for `wsk list` and `wsk namespace get` --- docs/actions.md | 28 ++- docs/apigateway.md | 30 ++-- tests/src/test/scala/common/Wsk.scala | 25 ++- .../whisk/core/cli/test/ApiGwTests.scala | 39 ++++- .../core/cli/test/WskBasicUsageTests.scala | 135 +++++++++++++++ tools/cli/go-whisk-cli/commands/action.go | 4 +- tools/cli/go-whisk-cli/commands/activation.go | 2 +- tools/cli/go-whisk-cli/commands/api.go | 120 ++++++++----- tools/cli/go-whisk-cli/commands/flags.go | 1 + tools/cli/go-whisk-cli/commands/namespace.go | 25 ++- tools/cli/go-whisk-cli/commands/package.go | 4 +- tools/cli/go-whisk-cli/commands/rule.go | 5 +- tools/cli/go-whisk-cli/commands/trigger.go | 4 +- tools/cli/go-whisk-cli/commands/util.go | 162 ++++++++++-------- tools/cli/go-whisk-cli/commands/wsk.go | 2 + .../wski18n/resources/en_US.all.json | 19 +- tools/cli/go-whisk/whisk/action.go | 45 +++++ tools/cli/go-whisk/whisk/activation.go | 20 +++ tools/cli/go-whisk/whisk/api.go | 89 +++++++++- tools/cli/go-whisk/whisk/namespace.go | 29 ++++ tools/cli/go-whisk/whisk/package.go | 39 +++++ tools/cli/go-whisk/whisk/rule.go | 32 +++- tools/cli/go-whisk/whisk/trigger.go | 34 +++- tools/cli/go-whisk/whisk/util.go | 15 ++ 24 files changed, 750 insertions(+), 158 deletions(-) diff --git a/docs/actions.md b/docs/actions.md index 4073154bfce..a1d161e137f 100644 --- a/docs/actions.md +++ b/docs/actions.md @@ -1028,13 +1028,37 @@ This command starts a polling loop that continuously checks for logs from activa ## Listing actions -You can list all the actions that you have created using: +You can list all the actions that you have created using `wsk action list`: ``` wsk action list +actions +/guest/packageB/A private nodejs:6 +/guest/C private nodejs:6 +/guest/A private nodejs:6 +/guest/packageA/B private nodejs:6 +/guest/packageA/A private nodejs:6 +/guest/B private nodejs:6 ``` -As you write more actions, this list gets longer and it can be helpful to group related actions into [packages](./packages.md). To filter your list of actions to just the those within a specific pacakge, you can use: +Here, we see actions listed in order from most to least recently updated. For easier browsing, you can use the flag `--name-sort` or `-n` to sort the list alphabetically: + +``` +wsk action list --name-sort +actions +/guest/A private nodejs:6 +/guest/B private nodejs:6 +/guest/C private nodejs:6 +/guest/packageA/A private nodejs:6 +/guest/packageA/B private nodejs:6 +/guest/packageB/A private nodejs:6 +``` + +Notice that the list is now sorted alphabetically by namespace, then package name, and finally action name, with the default package (no specified package) listed at the top. + +**Note**: The printed list is sorted alphabetically after it is received from the server. Other list flags such as `--limit` and `--skip` will be applied to the block of actions before they are received for sorting. To list actions in order by creation time, use the flag `--time`. + +As you write more actions, this list gets longer and it can be helpful to group related actions into [packages](./packages.md). To filter your list of actions to just those within a specific package, you can use: ``` wsk action list [PACKAGE NAME] diff --git a/docs/apigateway.md b/docs/apigateway.md index 50dcd61b209..609656d4e8f 100644 --- a/docs/apigateway.md +++ b/docs/apigateway.md @@ -22,18 +22,18 @@ Follow the instructions in [Configure CLI](./README.md#setting-up-the-openwhisk- return {payload: `Hello world ${name}`}; } ``` - + 2. Create a web action from the following JavaScript function. For this example, the action is called 'hello'. Make sure to add the flag `--web true` - + ``` wsk action create hello hello.js --web true ``` ``` ok: created action hello ``` - + 3. Create an API with base path `/hello`, path `/world` and method `get` with response type `json` - + ``` wsk api create /hello /world get hello --response-type json ``` @@ -42,9 +42,9 @@ Follow the instructions in [Configure CLI](./README.md#setting-up-the-openwhisk- https://${APIHOST}:9001/api/21ef035/hello/world ``` A new URL is generated exposing the `hello` action via a __GET__ HTTP method. - + 4. Let's give it a try by sending a HTTP request to the URL. - + ``` $ curl https://${APIHOST}:9001/api/21ef035/hello/world?name=OpenWhisk ``` @@ -54,12 +54,12 @@ Follow the instructions in [Configure CLI](./README.md#setting-up-the-openwhisk- } ``` The web action `hello` was invoked, returning back a JSON object including the parameter `name` sent via query parameter. You can pass parameters to the action via simple query parameters, or via the request body. Web actions allow you to invoke an action in a public way without the OpenWhisk authorization API key. - + ### Full control over the HTTP response - - The `--response-type` flag controls the target URL of the web action to be proxied by the API Gateway. Using `--response-type json` as above returns the full result of the action in JSON format and automatically sets the Content-Type header to `application/json` which enables you to easily get started. - - Once you get started you want to have full control over the HTTP response properties like `statusCode`, `headers` and return different content types in the `body`. You can do this by using `--response-type http`, this will configure the target URL of the web action with the `http` extension. + + The `--response-type` flag controls the target URL of the web action to be proxied by the API Gateway. Using `--response-type json` as above returns the full result of the action in JSON format and automatically sets the Content-Type header to `application/json` which enables you to easily get started. + + Once you get started, you will want to have full control over the HTTP response properties like `statusCode`, `headers` and return different content types in the `body`. You can do this by using `--response-type http`, this will configure the target URL of the web action with the `http` extension. You can choose to change the code of the action to comply with the return of web actions with `http` extension or include the action in a sequence passing its result to a new action that transforms the result to be properly formatted for an HTTP response. You can read more about response types and web actions extensions in the [Web Actions](webactions.md) documentation. @@ -67,14 +67,14 @@ Follow the instructions in [Configure CLI](./README.md#setting-up-the-openwhisk- ```javascript function main({name:name='Serverless API'}) { return { - body: new Buffer(JSON.stringify({payload:`Hello world ${name}`})).toString('base64'), - statusCode:200, + body: new Buffer(JSON.stringify({payload:`Hello world ${name}`})).toString('base64'), + statusCode: 200, headers:{ 'Content-Type': 'application/json'} }; } ``` Notice that the body needs to be return encoded in `base64` and not a string. - + Update the action with the modified result ``` wsk action update hello hello.js --web true @@ -170,7 +170,7 @@ curl -X GET https://${APIHOST}:9001/api/21ef035/club/books ``` ### Exporting the configuration -Let's export API named `Book Club` into a file that we can use as a base to to re-create the APIs using a file as input. +Let's export API named `Book Club` into a file that we can use as a base to to re-create the APIs using a file as input. ``` wsk api get "Book Club" > club-swagger.json ``` diff --git a/tests/src/test/scala/common/Wsk.scala b/tests/src/test/scala/common/Wsk.scala index 79a10547aed..8b1ad6cd73a 100644 --- a/tests/src/test/scala/common/Wsk.scala +++ b/tests/src/test/scala/common/Wsk.scala @@ -144,10 +144,12 @@ trait ListOrGetFromCollection extends FullyQualifiedNames { def list( namespace: Option[String] = None, limit: Option[Int] = None, + nameSort: Option[Boolean] = None, expectedExitCode: Int = SUCCESS_EXIT)( implicit wp: WskProps): RunResult = { val params = Seq(noun, "list", resolve(namespace), "--auth", wp.authKey) ++ - { limit map { l => Seq("--limit", l.toString) } getOrElse Seq() } + { limit map { l => Seq("--limit", l.toString) } getOrElse Seq() } ++ + { nameSort map { n => Seq("--name-sort") } getOrElse Seq() } cli(wp.overrides ++ params, expectedExitCode) } @@ -690,9 +692,12 @@ class WskNamespace() * @param expectedExitCode (optional) the expected exit code for the command * if the code is anything but DONTCARE_EXIT, assert the code is as expected */ - def list(expectedExitCode: Int = SUCCESS_EXIT)( + def list( + expectedExitCode: Int = SUCCESS_EXIT, + nameSort: Option[Boolean] = None)( implicit wp: WskProps): RunResult = { - val params = Seq(noun, "list", "--auth", wp.authKey) + val params = Seq(noun, "list", "--auth", wp.authKey) ++ + { nameSort map { n => Seq("--name-sort") } getOrElse Seq() } cli(wp.overrides ++ params, expectedExitCode) } @@ -718,9 +723,11 @@ class WskNamespace() */ def get( namespace: Option[String] = None, - expectedExitCode: Int)( + expectedExitCode: Int, + nameSort: Option[Boolean] = None)( implicit wp: WskProps): RunResult = { - cli(wp.overrides ++ Seq(noun, "get", resolve(namespace), "--auth", wp.authKey), expectedExitCode) + val params = { nameSort map { n => Seq("--name-sort") } getOrElse Seq() } + cli(wp.overrides ++ Seq(noun, "get", resolve(namespace), "--auth", wp.authKey) ++ params, expectedExitCode) } } @@ -818,6 +825,7 @@ class WskApiExperimental extends RunWskCmd { limit: Option[Int] = None, since: Option[Instant] = None, full: Option[Boolean] = None, + nameSort: Option[Boolean] = None, expectedExitCode: Int = SUCCESS_EXIT)( implicit wp: WskProps): RunResult = { val params = Seq(noun, "list", "--auth", wp.authKey) ++ @@ -826,7 +834,8 @@ class WskApiExperimental extends RunWskCmd { { operation map { o => Seq(o) } getOrElse Seq() } ++ { limit map { l => Seq("--limit", l.toString) } getOrElse Seq() } ++ { since map { i => Seq("--since", i.toEpochMilli.toString) } getOrElse Seq() } ++ - { full map { r => Seq("--full") } getOrElse Seq() } + { full map { r => Seq("--full") } getOrElse Seq() } ++ + { nameSort map { n => Seq("--name-sort") } getOrElse Seq() } cli(wp.overrides ++ params, expectedExitCode, showCmd = true) } @@ -912,6 +921,7 @@ class WskApi() limit: Option[Int] = None, since: Option[Instant] = None, full: Option[Boolean] = None, + nameSort: Option[Boolean] = None, expectedExitCode: Int = SUCCESS_EXIT, cliCfgFile: Option[String] = None)( implicit wp: WskProps): RunResult = { @@ -921,7 +931,8 @@ class WskApi() { operation map { o => Seq(o) } getOrElse Seq() } ++ { limit map { l => Seq("--limit", l.toString) } getOrElse Seq() } ++ { since map { i => Seq("--since", i.toEpochMilli.toString) } getOrElse Seq() } ++ - { full map { r => Seq("--full") } getOrElse Seq() } + { full map { r => Seq("--full") } getOrElse Seq() } ++ + { nameSort map { n => Seq("--name-sort") } getOrElse Seq() } cli(wp.overrides ++ params, expectedExitCode, showCmd = true, env = Map("WSK_CONFIG_FILE" -> cliCfgFile.getOrElse(""))) } diff --git a/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala b/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala index e7abf10eabe..d88cb98a9b4 100644 --- a/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala +++ b/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala @@ -131,11 +131,12 @@ class ApiGwTests limit: Option[Int] = None, since: Option[Instant] = None, full: Option[Boolean] = None, + nameSort: Option[Boolean] = None, expectedExitCode: Int = SUCCESS_EXIT, cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath())): RunResult = { checkThrottle() - wsk.api.list(basepathOrApiName, relpath, operation, limit, since, full, expectedExitCode, cliCfgFile) + wsk.api.list(basepathOrApiName, relpath, operation, limit, since, full, nameSort, expectedExitCode, cliCfgFile) } def apiGet( @@ -906,4 +907,40 @@ class ApiGwTests var deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) } } + + it should "list api alphabetically by Base/Rel/Verb" in { + val baseName = "/BaseTestPathApiList" + val actionName = "actionName" + val file = TestUtils.getTestActionFilename(s"echo-web-http.js") + try { + // Create Action for apis + var action = wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + println("action creation: " + action.stdout) + // Create apis + for (i <- 1 to 3) { + val base = s"$baseName$i" + var api = apiCreate( + basepath = Some(base), + relpath = Some("/relPath"), + operation = Some("GET"), + action = Some(actionName)) + println("api creation: " + api.stdout) + } + val original = apiList(nameSort = Some(true)).stdout + val originalFull = apiList(full = Some(true), nameSort = Some(true)).stdout + val scalaSorted = List(s"${baseName}1" + "/", s"${baseName}2" + "/", s"${baseName}3" + "/") + val regex = s"${baseName}[1-3]/".r + val list = (regex.findAllMatchIn(original)).toList + val listFull = (regex.findAllMatchIn(originalFull)).toList + + scalaSorted.toString shouldEqual list.toString + scalaSorted.toString shouldEqual listFull.toString + } finally { + // Clean up Apis + for (i <- 1 to 3) { + val deleteApis = apiDelete(basepathOrApiName = s"${baseName}$i", expectedExitCode = DONTCARE_EXIT) + } + val deleteAction = wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) + } + } } 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 c451bbdb482..3f3c7304b5b 100644 --- a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala +++ b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala @@ -1233,6 +1233,141 @@ class WskBasicUsageTests }, 5, Some(1 second)) } + it should "return a list of alphabetized actions" in withAssetCleaner(wskprops) { + (wp, assetHelper) => + // Declare 4 actions, create them out of alphabetical order + val actionName = "actionAlphaTest" + for (i <- 1 to 3) { + val name = s"$actionName$i" + assetHelper.withCleaner(wsk.action, name) { + (action, name) => + action.create(name, defaultAction) + } + } + retry({ + val original = wsk.action.list(nameSort = Some(true)).stdout + // Create list with action names in correct order + val scalaSorted = List(s"${actionName}1", s"${actionName}2", s"${actionName}3") + // Filter out everything not previously created + val regex = s"${actionName}[1-3]".r + // Retrieve action names into list as found in original + val list = (regex.findAllMatchIn(original)).toList + scalaSorted.toString shouldEqual list.toString + }, 5, Some(1 second)) + } + + it should "return an alphabetized list with default package actions on top" in withAssetCleaner(wskprops) { + (wp, assetHelper) => + // Declare 4 actions, create them out of alphabetical order + val actionName = "actionPackageAlphaTest" + val packageName = "packageAlphaTest" + assetHelper.withCleaner(wsk.action, actionName) { + (action, actionName) => + action.create(actionName, defaultAction) + } + assetHelper.withCleaner(wsk.pkg, packageName) { + (pkg, packageName) => + pkg.create(packageName) + } + for (i <- 1 to 3) { + val name = s"${packageName}/${actionName}$i" + assetHelper.withCleaner(wsk.action, name) { + (action, name) => + action.create(name, defaultAction) + } + } + retry({ + val original = wsk.action.list(nameSort = Some(true)).stdout + // Create list with action names in correct order + val scalaSorted = List(s"$actionName", s"${packageName}/${actionName}1", s"${packageName}/${actionName}2", s"${packageName}/${actionName}3") + // Filter out everything not previously created + val regexNoPackage = s"$actionName".r + val regexWithPackage = s"${packageName}/${actionName}[1-3]".r + // Retrieve action names into list as found in original + val list = regexNoPackage.findFirstIn(original).get :: (regexWithPackage.findAllMatchIn(original)).toList + scalaSorted.toString shouldEqual list.toString + }, 5, Some(1 second)) + } + + it should "return a list of alphabetized packages" in withAssetCleaner(wskprops) { + (wp, assetHelper) => + // Declare 3 packages, create them out of alphabetical order + val packageName = "pkgAlphaTest" + for (i <- 1 to 3) { + val name = s"$packageName$i" + assetHelper.withCleaner(wsk.pkg, name) { + (pkg, name) => + pkg.create(name) + } + } + retry({ + val original = wsk.pkg.list(nameSort = Some(true)).stdout + // Create list with package names in correct order + val scalaSorted = List(s"${packageName}1", s"${packageName}2", s"${packageName}3") + // Filter out everything not previously created + val regex = s"${packageName}[1-3]".r + // Retrieve package names into list as found in original + val list = (regex.findAllMatchIn(original)).toList + scalaSorted.toString shouldEqual list.toString + }, 5, Some(1 second)) + } + + it should "return a list of alphabetized triggers" in withAssetCleaner(wskprops) { + (wp, assetHelper) => + // Declare 4 triggers, create them out of alphabetical order + val triggerName = "triggerAlphaTest" + for (i <- 1 to 3) { + val name = s"$triggerName$i" + assetHelper.withCleaner(wsk.trigger, name) { + (trigger, name) => + trigger.create(name) + } + } + retry({ + val original = wsk.trigger.list(nameSort = Some(true)).stdout + // Create list with trigger names in correct order + val scalaSorted = List(s"${triggerName}1", s"${triggerName}2", s"${triggerName}3") + // Filter out everything not previously created + val regex = s"${triggerName}[1-3]".r + // Retrieve trigger names into list as found in original + val list = (regex.findAllMatchIn(original)).toList + scalaSorted.toString shouldEqual list.toString + }, 5, Some(1 second)) + } + + it should "return a list of alphabetized rules" in withAssetCleaner(wskprops) { + (wp, assetHelper) => + // Declare a trigger and an action for the purposes of creating rules + val triggerName = "listRulesTrigger" + val actionName = "listRulesAction" + + assetHelper.withCleaner(wsk.trigger, triggerName) { + (trigger, name) => trigger.create(name) + } + assetHelper.withCleaner(wsk.action, actionName) { + (action, name) => action.create(name, defaultAction) + } + // Declare 3 rules, create them out of alphabetical order + val ruleName = "ruleAlphaTest" + for (i <- 1 to 3) { + val name = s"$ruleName$i" + assetHelper.withCleaner(wsk.rule, name) { + (rule, name) => + rule.create(name, trigger = triggerName, action = actionName) + } + } + retry({ + val original = wsk.rule.list(nameSort = Some(true)).stdout + // Create list with rule names in correct order + val scalaSorted = List(s"${ruleName}1", s"${ruleName}2", s"${ruleName}3") + // Filter out everything not previously created + val regex = s"${ruleName}[1-3]".r + // Retrieve rule names into list as found in original + val list = (regex.findAllMatchIn(original)).toList + scalaSorted.toString shouldEqual list.toString + }) + } + behavior of "Wsk params and annotations" it should "reject commands that are executed with invalid JSON for annotations and parameters" in { diff --git a/tools/cli/go-whisk-cli/commands/action.go b/tools/cli/go-whisk-cli/commands/action.go index 3008a57429f..bfa62944ef6 100644 --- a/tools/cli/go-whisk-cli/commands/action.go +++ b/tools/cli/go-whisk-cli/commands/action.go @@ -320,7 +320,8 @@ var actionListCmd = &cobra.Command{ return actionListError(qualifiedName.GetEntityName(), options, err) } - printList(actions) + sortByName := flags.common.nameSort + printList(actions, sortByName) return nil }, @@ -941,6 +942,7 @@ func init() { 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")) + actionListCmd.Flags().BoolVarP(&flags.common.nameSort, "name-sort", "n", false, wski18n.T("sorts a list alphabetically by entity name; only applicable within the limit/skip returned entity block")) actionCmd.AddCommand( actionCreateCmd, diff --git a/tools/cli/go-whisk-cli/commands/activation.go b/tools/cli/go-whisk-cli/commands/activation.go index 4ae62fe1d0b..59e84fe82bc 100644 --- a/tools/cli/go-whisk-cli/commands/activation.go +++ b/tools/cli/go-whisk-cli/commands/activation.go @@ -93,7 +93,7 @@ var activationListCmd = &cobra.Command{ if options.Docs == true { printFullActivationList(activations) } else { - printList(activations) + printList(activations, false) // Default sorting for Activations are by creation time, hence sortByName is always false } return nil diff --git a/tools/cli/go-whisk-cli/commands/api.go b/tools/cli/go-whisk-cli/commands/api.go index b4c43068b82..bd60492ff4b 100644 --- a/tools/cli/go-whisk-cli/commands/api.go +++ b/tools/cli/go-whisk-cli/commands/api.go @@ -93,7 +93,6 @@ func isValidRelpath(relpath string) (error, bool) { return nil, true } - /* * Pull the managedUrl (external API URL) from the API configuration */ @@ -445,6 +444,8 @@ var apiListCmd = &cobra.Command{ var retApiArray *whisk.RetApiArray var apiPath string var apiVerb string + var orderFilteredList []whisk.ApiFilteredList + var orderFilteredRow []whisk.ApiFilteredRow if whiskErr := checkArgs(args, 0, 3, "Api list", wski18n.T("Optional parameters are: API base path (or API name), API relative path and operation.")); whiskErr != nil { @@ -518,7 +519,8 @@ var apiListCmd = &cobra.Command{ // Cast to a common type to allow for code to print out apilist response or apiget response retApiArray = (*whisk.RetApiArray)(retApi) } - + //Checks for any order flags being passed + sortByName := flags.common.nameSort // Display the APIs - applying any specified filtering if (flags.common.full) { fmt.Fprintf(color.Output, @@ -526,10 +528,10 @@ var apiListCmd = &cobra.Command{ map[string]interface{}{ "ok": color.GreenString("ok:"), })) - - for i:=0; i 0) { // Dynamically create the output format string based on the maximum size of the @@ -542,17 +544,17 @@ var apiListCmd = &cobra.Command{ map[string]interface{}{ "ok": color.GreenString("ok:"), })) - fmt.Printf(fmtString, "Action", "Verb", "API Name", "URL") - for i:=0; i 0 { + quickSort(commandToSort, 0, len(commandToSort)-1) } + printCommandsList(toPrintable(commandToSort), makeDefaultHeader(collection)) +} + +func quickSort(toSort Sortables, left int, right int) { + low := left + high := right + pivot := toSort[(left + right) / 2] + + for low <= high { + for toSort[low].Compare(pivot) { low++ } + for pivot.Compare(toSort[high]) { high-- } + if low <= high { + Swap(toSort, low, high) + low++ + high-- + } + } + if left < high { quickSort(toSort, left, high) } + if low < right { quickSort(toSort, low, right) } +} + +// makeDefaultHeader(collection) returns the default header to be used in case +// the list to be printed is empty. +func makeDefaultHeader(collection interface{}) string { + defaultHeader := reflect.TypeOf(collection).String() + defaultHeader = strings.ToLower(defaultHeader[8:] + "s") // Removes '[]whisk.' from `[]whisk.ENTITY_TYPE` + if defaultHeader == "apifilteredrows" { + defaultHeader = fmt.Sprintf("%-30s %7s %20s %s", "Action", "Verb", "API Name", "URL") + } else if defaultHeader == "apifilteredlists" { + defaultHeader = "" + } + return defaultHeader } func printFullList(collection interface{}) { @@ -178,53 +249,18 @@ func printSummary(collection interface{}) { } } -func printActionList(actions []whisk.Action) { - fmt.Fprintf(color.Output, "%s\n", boldString("actions")) - for _, action := range actions { - publishState := wski18n.T("private") - kind := getValueString(action.Annotations, "exec") - fmt.Printf("%-70s %s %s\n", fmt.Sprintf("/%s/%s", action.Namespace, action.Name), publishState, kind) - } -} - -func printTriggerList(triggers []whisk.Trigger) { - fmt.Fprintf(color.Output, "%s\n", boldString("triggers")) - for _, trigger := range triggers { - publishState := wski18n.T("private") - fmt.Printf("%-70s %s\n", fmt.Sprintf("/%s/%s", trigger.Namespace, trigger.Name), publishState) - } -} - -func printPackageList(packages []whisk.Package) { - fmt.Fprintf(color.Output, "%s\n", boldString("packages")) - for _, xPackage := range packages { - publishState := wski18n.T("private") - if xPackage.Publish != nil && *xPackage.Publish { - publishState = wski18n.T("shared") +// Used to print Action, Tigger, Package, and Rule lists +// Param: Takes in a array of Printable interface, and the name of the command +// being sent to it +// **Note**: The name sould be an empty string for APIs. +func printCommandsList(commands []whisk.Printable, defaultHeader string) { + if len(commands) != 0 { + fmt.Fprint(color.Output, boldString(commands[0].ToHeaderString())) + for i := range commands { + fmt.Print(commands[i].ToSummaryRowString()) } - fmt.Printf("%-70s %s\n", fmt.Sprintf("/%s/%s", xPackage.Namespace, xPackage.Name), publishState) - } -} - -func printRuleList(rules []whisk.Rule) { - fmt.Fprintf(color.Output, "%s\n", boldString("rules")) - for _, rule := range rules { - publishState := wski18n.T("private") - fmt.Printf("%-70s %-20s %s\n", fmt.Sprintf("/%s/%s", rule.Namespace, rule.Name), publishState, rule.Status) - } -} - -func printNamespaceList(namespaces []whisk.Namespace) { - fmt.Fprintf(color.Output, "%s\n", boldString("namespaces")) - for _, namespace := range namespaces { - fmt.Printf("%s\n", namespace.Name) - } -} - -func printActivationList(activations []whisk.Activation) { - fmt.Fprintf(color.Output, "%s\n", boldString("activations")) - for _, activation := range activations { - fmt.Printf("%s %-20s\n", activation.ActivationID, activation.Name) + } else { + fmt.Fprintf(color.Output, "%s\n", boldString(defaultHeader)) } } @@ -235,20 +271,6 @@ func printFullActivationList(activations []whisk.Activation) { } } -func printApiList(apis []whisk.Api) { - fmt.Fprintf(color.Output, "%s\n", boldString("apis")) - for _, api := range apis { - fmt.Printf("%s %20s %20s\n", api.ApiName, api.GatewayBasePath, api.GatewayFullPath) - } -} - -func printFullApiList(apis []whisk.Api) { - fmt.Fprintf(color.Output, "%s\n", boldString("apis")) - for _, api := range apis { - printJSON(api) - } -} - func printActivationLogs(logs []string) { for _, log := range logs { fmt.Printf("%s\n", log) diff --git a/tools/cli/go-whisk-cli/commands/wsk.go b/tools/cli/go-whisk-cli/commands/wsk.go index b4bbc5b6edc..8d520e9bb7c 100644 --- a/tools/cli/go-whisk-cli/commands/wsk.go +++ b/tools/cli/go-whisk-cli/commands/wsk.go @@ -44,6 +44,8 @@ func init() { WskCmd.SetHelpTemplate(`{{with or .Long .Short }}{{.}} {{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`) +listCmd.Flags().BoolVarP(&flags.common.nameSort, "name-sort", "n", false, wski18n.T("sorts a list alphabetically by entity name; only applicable within the limit/skip returned entity block")) + WskCmd.AddCommand( actionCmd, activationCmd, 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 78acda07838..39551d07212 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 @@ -1400,8 +1400,8 @@ "translation": "display full description of each API" }, { - "id": "An API host must be provided.", - "translation": "An API host must be provided." + "id": "order API list by action name first followed by base-path/rel-path/verb", + "translation": "order API list by action name first followed by base-path/rel-path/verb." }, { "id": "Request accepted, but processing not completed yet.", @@ -1510,9 +1510,16 @@ { "id": "Unable to output bash command completion {{.err}}", "translation": "Unable to output bash command completion {{.err}}" -}, -{ + }, + { "id": "prints bash command completion script to stdout", "translation": "prints bash command completion script to stdout" -} -] + }, + { + "id": "sorts a list alphabetically by entity name; only applicable within the limit/skip returned entity block", + "translation": "sorts a list alphabetically by entity name; only applicable within the limit/skip returned entity block" + }, + { + "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" + }] diff --git a/tools/cli/go-whisk/whisk/action.go b/tools/cli/go-whisk/whisk/action.go index a129048c3d0..8b17d23617e 100644 --- a/tools/cli/go-whisk/whisk/action.go +++ b/tools/cli/go-whisk/whisk/action.go @@ -59,6 +59,51 @@ type ActionListOptions struct { Docs bool `url:"docs,omitempty"` } +// Compare(sortable) compares action to sortable for the purpose of sorting. +// REQUIRED: sortable must also be of type Action. +// ***Method of type Sortable*** +func(action Action) Compare(sortable Sortable) (bool) { + // Sorts alphabetically by NAMESPACE -> PACKAGE_NAME -> ACTION_NAME, with + // actions under default package at the top. + var actionString string + var compareString string + actionToCompare := sortable.(Action) + + actionString = strings.ToLower(fmt.Sprintf("%s%s", action.Namespace, action.Name)) + compareString = strings.ToLower(fmt.Sprintf("%s%s", actionToCompare.Namespace, + actionToCompare.Name)) + if strings.Contains(action.Namespace, "/") && !strings.Contains(actionToCompare.Namespace, "/") { + return false + } else if !strings.Contains(action.Namespace, "/") && strings.Contains(actionToCompare.Namespace, "/") { + return true + } else if strings.Contains(action.Namespace, "/") && strings.Contains(actionToCompare.Namespace, "/") { + return actionString < compareString + } else { + return action.Name < actionToCompare.Name + } +} + +// ToHeaderString() returns the header for a list of actions +func(action Action) ToHeaderString() string { + return fmt.Sprintf("%s\n", "actions") +} + +// ToSummaryRowString() returns a compound string of required parameters for printing +// from CLI command `wsk action list`. +// ***Method of type Sortable*** +func(action Action) ToSummaryRowString() string{ + var kind string + publishState := wski18n.T("private") + + for i := range action.Annotations { + if (action.Annotations[i].Key == "exec") { + kind = action.Annotations[i].Value.(string) + break + } + } + return fmt.Sprintf("%-70s %s %s\n", fmt.Sprintf("/%s/%s", action.Namespace, action.Name), publishState, kind) +} + /* Determines if an action is a web action by examining the action's annotations. A value of true is returned if the action's annotations contains a "web-export" key and its associated value is a boolean value of "true". Otherwise, false diff --git a/tools/cli/go-whisk/whisk/activation.go b/tools/cli/go-whisk/whisk/activation.go index 84308f3314a..c36cdbec54d 100644 --- a/tools/cli/go-whisk/whisk/activation.go +++ b/tools/cli/go-whisk/whisk/activation.go @@ -70,6 +70,26 @@ type Log struct { Time string `json:"time,omitempty"` } +// Compare(sortable) compares activation to sortable for the purpose of sorting. +// REQUIRED: sortable must also be of type Activation. +// ***Method of type Sortable*** +// ***Currently, no method of sorting defined*** +func(activation Activation) Compare(sortable Sortable) (bool) { + return true +} + +// ToHeaderString() returns the header for a list of activations +func(activation Activation) ToHeaderString() string { + return fmt.Sprintf("%s\n", "activations") +} + +// ToSummaryRowString() returns a compound string of required parameters for printing +// from CLI command `wsk activation list`. +// ***Method of type Sortable*** +func(activation Activation) ToSummaryRowString() string { + return fmt.Sprintf("%s %-20s\n", activation.ActivationID, activation.Name) +} + func (s *ActivationService) List(options *ActivationListOptions) ([]Activation, *http.Response, error) { // TODO :: for some reason /activations only works with "_" as namespace s.client.Namespace = "_" diff --git a/tools/cli/go-whisk/whisk/api.go b/tools/cli/go-whisk/whisk/api.go index 474ad2abcb4..cc11118dcc2 100644 --- a/tools/cli/go-whisk/whisk/api.go +++ b/tools/cli/go-whisk/whisk/api.go @@ -21,6 +21,8 @@ import ( "net/http" "errors" "../wski18n" + "strings" + "fmt" ) type ApiService struct { @@ -143,6 +145,27 @@ type ApiSwaggerOpXOpenWhisk struct { ApiUrl string `json:"url"` } +// Used for printing individual APIs in non-truncated form +type ApiFilteredList struct { + ActionName string + ApiName string + BasePath string + RelPath string + Verb string + Url string +} + +// Used for printing individual APIs in truncated form +type ApiFilteredRow struct { + ActionName string + ApiName string + BasePath string + RelPath string + Verb string + Url string + FmtString string +} + var ApiVerbs map[string]bool = map[string]bool { "GET": true, "PUT": true, @@ -162,6 +185,70 @@ const ( // Api Methods // ///////////////// +// Compare(sortable) compares api to sortable for the purpose of sorting. +// REQUIRED: sortable must also be of type ApiFilteredList. +// ***Method of type Sortable*** +func(api ApiFilteredList) Compare(sortable Sortable) (bool) { + // Sorts alphabetically by [BASE_PATH | API_NAME] -> REL_PATH -> API_VERB + apiToCompare := sortable.(ApiFilteredList) + var apiString string + var compareString string + + apiString = strings.ToLower(fmt.Sprintf("%s%s%s",api.BasePath, api.RelPath, + api.Verb)) + compareString = strings.ToLower(fmt.Sprintf("%s%s%s", apiToCompare.BasePath, + apiToCompare.RelPath, apiToCompare.Verb)) + + return apiString < compareString +} + +// ToHeaderString() returns the header for a list of apis +func(api ApiFilteredList) ToHeaderString() string { + return "" +} + +// ToSummaryRowString() returns a compound string of required parameters for printing +// from CLI command `wsk api list` or `wsk api-experimental list`. +// ***Method of type Sortable*** +func(api ApiFilteredList) ToSummaryRowString() string { + return fmt.Sprintf("%s %s %s %s %s %s", + fmt.Sprintf("%s: %s\n", wski18n.T("Action"), api.ActionName), + fmt.Sprintf(" %s: %s\n", wski18n.T("API Name"), api.ApiName), + fmt.Sprintf(" %s: %s\n", wski18n.T("Base path"), api.BasePath), + fmt.Sprintf(" %s: %s\n", wski18n.T("Path"), api.RelPath), + fmt.Sprintf(" %s: %s\n", wski18n.T("Verb"), api.Verb), + fmt.Sprintf(" %s: %s\n", wski18n.T("URL"), api.Url)) +} + +// Compare(sortable) compares api to sortable for the purpose of sorting. +// REQUIRED: sortable must also be of type ApiFilteredRow. +// ***Method of type Sortable*** +func(api ApiFilteredRow) Compare(sortable Sortable) (bool) { + // Sorts alphabetically by [BASE_PATH | API_NAME] -> REL_PATH -> API_VERB + var apiString string + var compareString string + apiToCompare := sortable.(ApiFilteredRow) + + apiString = strings.ToLower(fmt.Sprintf("%s%s%s",api.BasePath, api.RelPath, + api.Verb)) + compareString = strings.ToLower(fmt.Sprintf("%s%s%s", apiToCompare.BasePath, + apiToCompare.RelPath, apiToCompare.Verb)) + + return apiString < compareString +} + +// ToHeaderString() returns the header for a list of apis +func(api ApiFilteredRow) ToHeaderString() string { + return fmt.Sprintf("%s", fmt.Sprintf(api.FmtString, "Action", "Verb", "API Name", "URL")) +} + +// ToSummaryRowString() returns a compound string of required parameters for printing +// from CLI command `wsk api list -f` or `wsk api-experimental list -f`. +// ***Method of type Sortable*** +func(api ApiFilteredRow) ToSummaryRowString() string { + return fmt.Sprintf(api.FmtString, api.ActionName, api.Verb, api.ApiName, api.Url) +} + func (s *ApiService) List(apiListOptions *ApiListRequestOptions) (*ApiListResponse, *http.Response, error) { route := "web/whisk.system/apimgmt/getApi.http" @@ -315,7 +402,7 @@ func (s *ApiService) Delete(api *ApiDeleteRequest, options *ApiDeleteRequestOpti func validateApiListResponse(apiList *ApiListResponse) error { - for i:=0; i PACKAGE_NAME + packageToCompare := sortable.(Package) + + var packageString string + var compareString string + + packageString = strings.ToLower(fmt.Sprintf("%s%s",xPackage.Namespace, + xPackage.Name)) + compareString = strings.ToLower(fmt.Sprintf("%s%s", packageToCompare.Namespace, + packageToCompare.Name)) + + return packageString < compareString +} + +// ToHeaderString() returns the header for a list of actions +func(pkg Package) ToHeaderString() string { + return fmt.Sprintf("%s\n", "packages") +} + +// ToSummaryRowString() returns a compound string of required parameters for printing +// from CLI command `wsk package list`. +// ***Method of type Sortable*** +func(xPackage Package) ToSummaryRowString() string{ + publishState := wski18n.T("private") + + if xPackage.Publish != nil && *xPackage.Publish { + publishState = wski18n.T("shared") + } + + return fmt.Sprintf("%-70s %s\n", fmt.Sprintf("/%s/%s", xPackage.Namespace, + xPackage.Name), publishState) +} + func (s *PackageService) List(options *PackageListOptions) ([]Package, *http.Response, error) { route := fmt.Sprintf("packages") routeUrl, err := addRouteOptions(route, options) diff --git a/tools/cli/go-whisk/whisk/rule.go b/tools/cli/go-whisk/whisk/rule.go index 59d41c25d6c..0180f2a459c 100644 --- a/tools/cli/go-whisk/whisk/rule.go +++ b/tools/cli/go-whisk/whisk/rule.go @@ -38,7 +38,6 @@ type Rule struct { Trigger interface{} `json:"trigger"` Action interface{} `json:"action"` Publish *bool `json:"publish,omitempty"` - } type RuleListOptions struct { @@ -47,6 +46,37 @@ type RuleListOptions struct { Docs bool `url:"docs,omitempty"` } +// Compare(sortable) compares rule to sortable for the purpose of sorting. +// REQUIRED: sortable must also be of type Rule. +// ***Method of type Sortable*** +func(rule Rule) Compare(sortable Sortable) (bool) { + // Sorts alphabetically by NAMESPACE -> PACKAGE_NAME + ruleToCompare := sortable.(Rule) + var ruleString string + var compareString string + + ruleString = strings.ToLower(fmt.Sprintf("%s%s",rule.Namespace, rule.Name)) + compareString = strings.ToLower(fmt.Sprintf("%s%s", ruleToCompare.Namespace, + ruleToCompare.Name)) + + return ruleString < compareString +} + +// ToHeaderString() returns the header for a list of rules +func(rule Rule) ToHeaderString() string { + return fmt.Sprintf("%s\n", "rules") +} + +// ToSummaryRowString() returns a compound string of required parameters for printing +// from CLI command `wsk rule list`. +// ***Method of type Sortable*** +func(rule Rule) ToSummaryRowString() string{ + publishState := wski18n.T("private") + + return fmt.Sprintf("%-70s %-20s %s\n", fmt.Sprintf("/%s/%s", rule.Namespace, + rule.Name), publishState, rule.Status) +} + func (s *RuleService) List(options *RuleListOptions) ([]Rule, *http.Response, error) { route := "rules" routeUrl, err := addRouteOptions(route, options) diff --git a/tools/cli/go-whisk/whisk/trigger.go b/tools/cli/go-whisk/whisk/trigger.go index 9540398225f..26b115bd629 100644 --- a/tools/cli/go-whisk/whisk/trigger.go +++ b/tools/cli/go-whisk/whisk/trigger.go @@ -23,6 +23,7 @@ import ( "errors" "net/url" "../wski18n" + "strings" ) type TriggerService struct { @@ -38,7 +39,6 @@ type Trigger struct { Parameters KeyValueArr `json:"parameters,omitempty"` Limits *Limits `json:"limits,omitempty"` Publish *bool `json:"publish,omitempty"` - } type TriggerListOptions struct { @@ -47,6 +47,38 @@ type TriggerListOptions struct { Docs bool `url:"docs,omitempty"` } +// Compare(sortable) compares trigger to sortable for the purpose of sorting. +// REQUIRED: sortable must also be of type Trigger. +// ***Method of type Sortable*** +func(trigger Trigger) Compare(sortable Sortable) (bool) { + // Sorts alphabetically by NAMESPACE -> TRIGGER_NAME + triggerToCompare := sortable.(Trigger) + var triggerString string + var compareString string + + triggerString = strings.ToLower(fmt.Sprintf("%s%s",trigger.Namespace, + trigger.Name)) + compareString = strings.ToLower(fmt.Sprintf("%s%s", triggerToCompare.Namespace, + triggerToCompare.Name)) + + return triggerString < compareString +} + +// ToHeaderString() returns the header for a list of triggers +func(trigger Trigger) ToHeaderString() string { + return fmt.Sprintf("%s\n", "triggers") +} + +// ToSummaryRowString() returns a compound string of required parameters for printing +// from CLI command `wsk trigger list`. +// ***Method of type Sortable*** +func(trigger Trigger) ToSummaryRowString() string { + publishState := wski18n.T("private") + + return fmt.Sprintf("%-70s %s\n", fmt.Sprintf("/%s/%s", trigger.Namespace, + trigger.Name), publishState) +} + func (s *TriggerService) List(options *TriggerListOptions) ([]Trigger, *http.Response, error) { route := "triggers" routeUrl, err := addRouteOptions(route, options) diff --git a/tools/cli/go-whisk/whisk/util.go b/tools/cli/go-whisk/whisk/util.go index 0757864fafc..f29f1e08e33 100644 --- a/tools/cli/go-whisk/whisk/util.go +++ b/tools/cli/go-whisk/whisk/util.go @@ -29,6 +29,21 @@ import ( "../wski18n" ) +// Sortable items are anything that needs to be sorted for listing purposes. +type Sortable interface { + // Compare(sortable) compares an two sortables and returns true + // if the item calling the Compare method is less than toBeCompared. + // Sorts alphabetically by default, can have other parameters to sort by + // passed by sortByName. + Compare(toBeCompared Sortable) (bool) +} + +// Printable items are anything that need to be printed for listing purposes. +type Printable interface { + ToHeaderString() string // Prints header information of a Printable + ToSummaryRowString() string // Prints summary info of one Printable +} + // addOptions adds the parameters in opt as URL query parameters to s. opt // must be a struct whose fields may contain "url" tags. func addRouteOptions(route string, options interface{}) (*url.URL, error) {