blob: c7b415766da992ce04a9d409c7152da2c496067e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package commands
import (
"bufio"
"bytes"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/apache/incubator-openwhisk-cli/wski18n"
"github.com/apache/incubator-openwhisk-client-go/whisk"
"encoding/json"
"github.com/fatih/color"
"github.com/ghodss/yaml"
"github.com/spf13/cobra"
"regexp"
)
const (
yamlFileExtension = "yaml"
ymlFileExtension = "yml"
formatOptionYaml = "yaml"
formatOptionJson = "json"
pathParamRegex = `\/\{([^\/]+)\}\/|\/\{([^\/]+)\}$|\{([^\/]+)}\/`
pathSegmentParamRegex = `^\/\{([^\/]+)\}\/$`
)
var apiCmd = &cobra.Command{
Use: "api",
Short: wski18n.T("work with APIs"),
}
var fmtString = "%-30s %7s %20s %s\n"
func IsValidApiVerb(verb string) (error, bool) {
// Is the API verb valid?
if _, ok := whisk.ApiVerbs[strings.ToUpper(verb)]; !ok {
whisk.Debug(whisk.DbgError, "Invalid API verb: '%s'\n", verb)
errMsg := wski18n.T("'{{.verb}}' is not a valid API verb. Valid values are: {{.verbs}}",
map[string]interface{}{
"verb": verb,
"verbs": reflect.ValueOf(whisk.ApiVerbs).MapKeys()})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return whiskErr, false
}
return nil, true
}
func hasPathPrefix(path string) (error, bool) {
if !strings.HasPrefix(path, "/") {
whisk.Debug(whisk.DbgError, "path does not begin with '/': '%s'\n", path)
errMsg := wski18n.T("'{{.path}}' must begin with '/'.",
map[string]interface{}{
"path": path,
})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return whiskErr, false
}
return nil, true
}
func isValidBasepath(basepath string) (error, bool) {
if whiskerr, ok := hasPathPrefix(basepath); !ok {
return whiskerr, false
}
return nil, true
}
func isValidRelpath(relpath string) (error, bool) {
if whiskerr, ok := hasPathPrefix(relpath); !ok {
return whiskerr, false
}
return nil, true
}
func getPathParameterNames(path string) ([]string, error) {
var pathParameters []string
regexObj, err := regexp.Compile(pathSegmentParamRegex)
if err != nil {
whisk.Debug(whisk.DbgError, "Failed to match path '%s' to regular expressions `%s`\n", path, pathSegmentParamRegex)
} else {
segments := strings.Split(path, "/")
for _, segment := range segments {
segment = fmt.Sprintf("/%s/", segment)
matchedItems := regexObj.FindAllStringSubmatch(segment, -1)
for _, matchedParam := range matchedItems {
for idx, paramName := range matchedParam {
whisk.Debug(whisk.DbgInfo, "Path parameter submatch '%v'; idx %v\n", paramName, idx)
if idx > 0 && len(paramName) > 0 {
pathParameters = append(pathParameters, paramName)
}
}
}
}
}
return pathParameters, err
}
func hasPathParameters(path string) (bool, error) {
pathParams, err := getPathParameterNames(path)
return len(pathParams) > 0, err
}
func isBasepathParameterized(basepath string) (error, bool) {
hasParams, err := hasPathParameters(basepath)
if hasParams || err != nil {
errMsg := wski18n.T("The base path '{{.path}}' cannot have parameters. Only the relative path supports path parameters.",
map[string]interface{}{"path": basepath})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return whiskErr, false
}
return nil, true
}
/*
* Pull the managedUrl (external API URL) from the API configuration
*/
func getManagedUrl(api *whisk.RetApi, relpath string, operation string) (url string) {
baseUrl := strings.TrimSuffix(api.BaseUrl, "/")
whisk.Debug(whisk.DbgInfo, "getManagedUrl: baseUrl = '%s', relpath = '%s', operation = '%s'\n", baseUrl, relpath, operation)
for path := range api.Swagger.Paths {
whisk.Debug(whisk.DbgInfo, "getManagedUrl: comparing api relpath: '%s'\n", path)
if path == relpath {
whisk.Debug(whisk.DbgInfo, "getManagedUrl: relpath matches '%s'\n", relpath)
for op := range api.Swagger.Paths[path].MakeOperationMap() {
whisk.Debug(whisk.DbgInfo, "getManagedUrl: comparing operation: '%s'\n", op)
if strings.ToLower(op) == strings.ToLower(operation) {
whisk.Debug(whisk.DbgInfo, "getManagedUrl: operation matches: '%s'\n", operation)
url = baseUrl + path
}
}
}
}
return url
}
//////////////
// Commands //
//////////////
var apiCreateCmd = &cobra.Command{
Use: "create ([BASE_PATH] API_PATH API_VERB ACTION] | --config-file CFG_FILE) ",
Short: wski18n.T("create a new API"),
SilenceUsage: true,
SilenceErrors: true,
PreRunE: SetupClientConfig,
RunE: func(cmd *cobra.Command, args []string) error {
var api *whisk.Api
var err error
var qname = new(QualifiedName)
var action *whisk.Action
var webactionSecret interface{}
if len(args) == 0 && Flags.api.configfile == "" {
whisk.Debug(whisk.DbgError, "No swagger file and no arguments\n")
errMsg := wski18n.T("Invalid argument(s). Specify a swagger file or specify an API base path with an API path, an API verb, and an action name.")
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return whiskErr
} else if len(args) == 0 && Flags.api.configfile != "" {
api, err = parseSwaggerApi(Flags.api.configfile, Client.Config.Namespace)
if err != nil {
whisk.Debug(whisk.DbgError, "parseSwaggerApi() error: %s\n", err)
errMsg := wski18n.T("Unable to parse swagger file: {{.err}}", map[string]interface{}{"err": err})
whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return whiskErr
}
} else {
if whiskErr := CheckArgs(args, 3, 4, "Api create",
wski18n.T("Specify a swagger file or specify an API base path with an API path, an API verb, and an action name.")); whiskErr != nil {
return whiskErr
}
api, qname, err = parseApi(cmd, args)
if err != nil {
whisk.Debug(whisk.DbgError, "parseApi(%s, %s) error: %s\n", cmd, args, err)
errMsg := wski18n.T("Unable to parse api command arguments: {{.err}}",
map[string]interface{}{"err": err})
whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return whiskErr
}
// Confirm that the specified action is a web-action
action, err = isWebAction(Client, *qname)
if err != nil {
whisk.Debug(whisk.DbgError, "isWebAction(%v) is false: %s\n", qname, err)
whiskErr := whisk.MakeWskError(err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return whiskErr
}
if webactionSecret = action.Annotations.GetValue(WEB_SECURE_ANNOT); webactionSecret != nil {
whisk.Debug(whisk.DbgInfo, "web action is secured\n")
}
}
apiCreateReq := new(whisk.ApiCreateRequest)
apiCreateReq.ApiDoc = api
apiCreateReqOptions := new(whisk.ApiCreateRequestOptions)
if apiCreateReqOptions.SpaceGuid, err = getUserContextId(); err != nil {
return err
}
if apiCreateReqOptions.AccessToken, err = getAccessToken(); err != nil {
return err
}
apiCreateReqOptions.ResponseType = Flags.api.resptype
if webactionSecret != nil {
apiCreateReq.ApiDoc.Action.SecureKey = webactionSecret
}
whisk.Debug(whisk.DbgInfo, "AccessToken: %s\nSpaceGuid: %s\nResponseType: %s",
apiCreateReqOptions.AccessToken, apiCreateReqOptions.SpaceGuid, apiCreateReqOptions.ResponseType)
retApi, _, err := Client.Apis.Insert(apiCreateReq, apiCreateReqOptions, whisk.DoNotOverwrite)
if err != nil {
whisk.Debug(whisk.DbgError, "Client.Apis.Insert(%#v, false) error: %s\n", api, err)
errMsg := wski18n.T("Unable to create API: {{.err}}", map[string]interface{}{"err": err})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return whiskErr
}
if api.Swagger == "" {
baseUrl := retApi.BaseUrl
fmt.Fprintf(color.Output,
wski18n.T("{{.ok}} created API {{.path}} {{.verb}} for action {{.name}}\n{{.fullpath}}\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
"path": strings.TrimSuffix(api.GatewayBasePath, "/") + api.GatewayRelPath,
"verb": api.GatewayMethod,
"name": boldString("/" + api.Action.Namespace + "/" + api.Action.Name),
"fullpath": strings.TrimSuffix(baseUrl, "/") + api.GatewayRelPath,
}))
} else {
whisk.Debug(whisk.DbgInfo, "Processing swagger based create API response\n")
baseUrl := retApi.BaseUrl
for path := range retApi.Swagger.Paths {
managedUrl := strings.TrimSuffix(baseUrl, "/") + path
whisk.Debug(whisk.DbgInfo, "Managed path: '%s'\n", managedUrl)
for op, opv := range retApi.Swagger.Paths[path].MakeOperationMap() {
whisk.Debug(whisk.DbgInfo, "Path operation: '%s'\n", op)
var fqActionName string
if opv.XOpenWhisk == nil {
fqActionName = ""
} else if len(opv.XOpenWhisk.Package) > 0 {
fqActionName = "/" + opv.XOpenWhisk.Namespace + "/" + opv.XOpenWhisk.Package + "/" + opv.XOpenWhisk.ActionName
} else {
fqActionName = "/" + opv.XOpenWhisk.Namespace + "/" + opv.XOpenWhisk.ActionName
}
whisk.Debug(whisk.DbgInfo, "baseUrl '%s' Path '%s' Path obj %+v\n", baseUrl, path, opv)
if len(fqActionName) > 0 {
fmt.Fprintf(color.Output,
wski18n.T("{{.ok}} created API {{.path}} {{.verb}} for action {{.name}}\n{{.fullpath}}\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
"path": strings.TrimSuffix(retApi.Swagger.BasePath, "/") + path,
"verb": op,
"name": boldString(fqActionName),
"fullpath": managedUrl,
}))
} else {
fmt.Fprintf(color.Output,
wski18n.T("{{.ok}} created API {{.path}} {{.verb}}\n{{.fullpath}}\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
"path": strings.TrimSuffix(retApi.Swagger.BasePath, "/") + path,
"verb": op,
"fullpath": managedUrl,
}))
}
}
}
}
return nil
},
}
var apiGetCmd = &cobra.Command{
Use: "get BASE_PATH | API_NAME",
Short: wski18n.T("get API details"),
SilenceUsage: true,
SilenceErrors: true,
PreRunE: SetupClientConfig,
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var isBasePathArg bool = true
if whiskErr := CheckArgs(args, 1, 1, "Api get",
wski18n.T("An API base path or API name is required.")); whiskErr != nil {
return whiskErr
}
if cmd.LocalFlags().Changed("format") &&
strings.ToLower(Flags.common.format) != formatOptionYaml &&
strings.ToLower(Flags.common.format) != formatOptionJson {
errMsg := wski18n.T("Invalid format type: {{.type}}", map[string]interface{}{"type": Flags.common.format})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return whiskErr
}
apiGetReq := new(whisk.ApiGetRequest)
apiGetReqOptions := new(whisk.ApiGetRequestOptions)
apiGetReqOptions.ApiBasePath = args[0]
if apiGetReqOptions.SpaceGuid, err = getUserContextId(); err != nil {
return err
}
if apiGetReqOptions.AccessToken, err = getAccessToken(); err != nil {
return err
}
retApi, _, err := Client.Apis.Get(apiGetReq, apiGetReqOptions)
if err != nil {
whisk.Debug(whisk.DbgError, "Client.Apis.Get(%#v, %#v) error: %s\n", apiGetReq, apiGetReqOptions, err)
errMsg := wski18n.T("Unable to get API '{{.name}}': {{.err}}", map[string]interface{}{"name": args[0], "err": err})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return whiskErr
}
whisk.Debug(whisk.DbgInfo, "Client.Apis.Get returned: %#v\n", retApi)
var displayResult interface{} = nil
if Flags.common.detail {
if retApi.Apis != nil && len(retApi.Apis) > 0 &&
retApi.Apis[0].ApiValue != nil {
displayResult = retApi.Apis[0].ApiValue
} else {
whisk.Debug(whisk.DbgError, "No result object returned\n")
}
} else {
if retApi.Apis != nil && len(retApi.Apis) > 0 &&
retApi.Apis[0].ApiValue != nil &&
retApi.Apis[0].ApiValue.Swagger != nil {
displayResult = retApi.Apis[0].ApiValue.Swagger
} else {
whisk.Debug(whisk.DbgError, "No swagger returned\n")
}
}
if displayResult == nil {
var errMsg string
if isBasePathArg {
errMsg = wski18n.T("API does not exist for basepath {{.basepath}}",
map[string]interface{}{"basepath": args[0]})
} else {
errMsg = wski18n.T("API does not exist for API name {{.apiname}}",
map[string]interface{}{"apiname": args[0]})
}
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return whiskErr
}
if cmd.LocalFlags().Changed("format") && strings.ToLower(Flags.common.format) == formatOptionYaml {
var jsonOutputBuffer bytes.Buffer
var jsonOutputWriter = bufio.NewWriter(&jsonOutputBuffer)
printJSON(displayResult, jsonOutputWriter)
jsonOutputWriter.Flush()
yamlbytes, err := yaml.JSONToYAML(jsonOutputBuffer.Bytes())
if err != nil {
whisk.Debug(whisk.DbgError, "yaml.JSONToYAML() error: %s\n", err)
errMsg := wski18n.T("Unable to convert API into YAML: {{.err}}", map[string]interface{}{"err": err})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return whiskErr
}
fmt.Println(string(yamlbytes))
} else {
printJSON(displayResult)
}
return nil
},
}
var apiDeleteCmd = &cobra.Command{
Use: "delete BASE_PATH | API_NAME [API_PATH [API_VERB]]",
Short: wski18n.T("delete an API"),
SilenceUsage: true,
SilenceErrors: true,
PreRunE: SetupClientConfig,
RunE: func(cmd *cobra.Command, args []string) error {
var err error
if whiskErr := CheckArgs(args, 1, 3, "Api delete",
wski18n.T("An API base path or API name is required. An optional API relative path and operation may also be provided.")); whiskErr != nil {
return whiskErr
}
apiDeleteReq := new(whisk.ApiDeleteRequest)
apiDeleteReqOptions := new(whisk.ApiDeleteRequestOptions)
if apiDeleteReqOptions.SpaceGuid, err = getUserContextId(); err != nil {
return err
}
if apiDeleteReqOptions.AccessToken, err = getAccessToken(); err != nil {
return err
}
// Is the argument a basepath (must start with /) or an API name
if _, ok := isValidBasepath(args[0]); !ok {
whisk.Debug(whisk.DbgInfo, "Treating '%s' as an API name; as it does not begin with '/'\n", args[0])
apiDeleteReqOptions.ApiBasePath = args[0]
} else {
apiDeleteReqOptions.ApiBasePath = args[0]
}
if len(args) > 1 {
// Is the API path valid?
if whiskErr, ok := isValidRelpath(args[1]); !ok {
return whiskErr
}
apiDeleteReqOptions.ApiRelPath = args[1]
}
if len(args) > 2 {
// Is the API verb valid?
if whiskErr, ok := IsValidApiVerb(args[2]); !ok {
return whiskErr
}
apiDeleteReqOptions.ApiVerb = strings.ToUpper(args[2])
}
_, err = Client.Apis.Delete(apiDeleteReq, apiDeleteReqOptions)
if err != nil {
whisk.Debug(whisk.DbgError, "Client.Apis.Delete(%#v, %#v) error: %s\n", apiDeleteReq, apiDeleteReqOptions, err)
errMsg := wski18n.T("Unable to delete API: {{.err}}", map[string]interface{}{"err": err})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return whiskErr
}
if len(args) == 1 {
fmt.Fprintf(color.Output,
wski18n.T("{{.ok}} deleted API {{.basepath}}\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
"basepath": apiDeleteReqOptions.ApiBasePath,
}))
} else if len(args) == 2 {
fmt.Fprintf(color.Output,
wski18n.T("{{.ok}} deleted {{.path}} from {{.basepath}}\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
"path": apiDeleteReqOptions.ApiRelPath,
"basepath": apiDeleteReqOptions.ApiBasePath,
}))
} else {
fmt.Fprintf(color.Output,
wski18n.T("{{.ok}} deleted {{.path}} {{.verb}} from {{.basepath}}\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
"path": apiDeleteReqOptions.ApiRelPath,
"verb": apiDeleteReqOptions.ApiVerb,
"basepath": apiDeleteReqOptions.ApiBasePath,
}))
}
return nil
},
}
var apiListCmd = &cobra.Command{
Use: "list [[BASE_PATH | API_NAME] [API_PATH [API_VERB]]",
Short: wski18n.T("list APIs"),
SilenceUsage: true,
SilenceErrors: true,
PreRunE: SetupClientConfig,
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var retApiList *whisk.ApiListResponse
var retApi *whisk.ApiGetResponse
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 {
return whiskErr
}
if len(args) == 0 {
// List API request query parameters
apiListReqOptions := new(whisk.ApiListRequestOptions)
apiListReqOptions.Limit = Flags.common.limit
apiListReqOptions.Skip = Flags.common.skip
if apiListReqOptions.SpaceGuid, err = getUserContextId(); err != nil {
return err
}
if apiListReqOptions.AccessToken, err = getAccessToken(); err != nil {
return err
}
retApiList, _, err = Client.Apis.List(apiListReqOptions)
if err != nil {
whisk.Debug(whisk.DbgError, "Client.Apis.List(%#v) error: %s\n", apiListReqOptions, err)
errMsg := wski18n.T("Unable to obtain the API list: {{.err}}", map[string]interface{}{"err": err})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return whiskErr
}
whisk.Debug(whisk.DbgInfo, "Client.Apis.List returned: %#v (%+v)\n", retApiList, retApiList)
// Cast to a common type to allow for code to print out apilist response or apiget response
retApiArray = (*whisk.RetApiArray)(retApiList)
} else {
// Get API request body
apiGetReq := new(whisk.ApiGetRequest)
apiGetReq.Namespace = Client.Config.Namespace
// Get API request options
apiGetReqOptions := new(whisk.ApiGetRequestOptions)
if apiGetReqOptions.SpaceGuid, err = getUserContextId(); err != nil {
return err
}
if apiGetReqOptions.AccessToken, err = getAccessToken(); err != nil {
return err
}
// The first argument is either a basepath (must start with /) or an API name
apiGetReqOptions.ApiBasePath = args[0]
if len(args) > 1 {
// Is the API path valid?
if whiskErr, ok := isValidRelpath(args[1]); !ok {
return whiskErr
}
apiPath = args[1]
apiGetReqOptions.ApiRelPath = apiPath
}
if len(args) > 2 {
// Is the API verb valid?
if whiskErr, ok := IsValidApiVerb(args[2]); !ok {
return whiskErr
}
apiVerb = strings.ToUpper(args[2])
apiGetReqOptions.ApiVerb = apiVerb
}
retApi, _, err = Client.Apis.Get(apiGetReq, apiGetReqOptions)
if err != nil {
whisk.Debug(whisk.DbgError, "Client.Apis.Get(%#v, %#v) error: %s\n", apiGetReq, apiGetReqOptions, err)
errMsg := wski18n.T("Unable to obtain the API list: {{.err}}", map[string]interface{}{"err": err})
whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return whiskErr
}
whisk.Debug(whisk.DbgInfo, "Client.Apis.Get returned: %#v\n", retApi)
// 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,
wski18n.T("{{.ok}} APIs\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
}))
for i := 0; i < len(retApiArray.Apis); i++ {
orderFilteredList = append(orderFilteredList, genFilteredList(retApiArray.Apis[i].ApiValue, apiPath, apiVerb)...)
}
printList(orderFilteredList, sortByName) // Sends an array of structs that contains specifed variables that are not truncated
} else {
if len(retApiArray.Apis) > 0 {
// Dynamically create the output format string based on the maximum size of the
// fully qualified action name and the API Name.
maxActionNameSize := min(40, max(len("Action"), getLargestActionNameSize(retApiArray, apiPath, apiVerb)))
maxApiNameSize := min(30, max(len("API Name"), getLargestApiNameSize(retApiArray, apiPath, apiVerb)))
fmtString = "%-" + strconv.Itoa(maxActionNameSize) + "s %7s %" + strconv.Itoa(maxApiNameSize+1) + "s %s\n"
fmt.Fprintf(color.Output,
wski18n.T("{{.ok}} APIs\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
}))
for i := 0; i < len(retApiArray.Apis); i++ {
orderFilteredRow = append(orderFilteredRow, genFilteredRow(retApiArray.Apis[i].ApiValue, apiPath, apiVerb, maxActionNameSize, maxApiNameSize)...)
}
printList(orderFilteredRow, sortByName) // Sends an array of structs that contains specifed variables that are truncated
} else {
fmt.Fprintf(color.Output,
wski18n.T("{{.ok}} APIs\n",
map[string]interface{}{
"ok": color.GreenString("ok:"),
}))
printList(orderFilteredRow, sortByName) // Sends empty orderFilteredRow so that defaultHeader can be printed
}
}
return nil
},
}
// genFilteredList(resultApi, api) generates an array of
// ApiFilteredLists for the purpose of ordering and printing in a list form.
// NOTE: genFilteredRow() generates entries with one line per configuration
// property (action name, verb, api name, api gw url)
func genFilteredList(resultApi *whisk.RetApi, apiPath string, apiVerb string) []whisk.ApiFilteredList {
var orderInfo whisk.ApiFilteredList
var orderInfoArr []whisk.ApiFilteredList
baseUrl := strings.TrimSuffix(resultApi.BaseUrl, "/")
apiName := resultApi.Swagger.Info.Title
basePath := resultApi.Swagger.BasePath
if resultApi.Swagger != nil && resultApi.Swagger.Paths != nil {
for path := range resultApi.Swagger.Paths {
whisk.Debug(whisk.DbgInfo, "genFilteredApi: comparing api relpath: '%s'\n", path)
if len(apiPath) == 0 || path == apiPath {
whisk.Debug(whisk.DbgInfo, "genFilteredList: relpath matches\n")
for op, opv := range resultApi.Swagger.Paths[path].MakeOperationMap() {
whisk.Debug(whisk.DbgInfo, "genFilteredList: comparing operation: '%s'\n", op)
if len(apiVerb) == 0 || strings.ToLower(op) == strings.ToLower(apiVerb) {
whisk.Debug(whisk.DbgInfo, "genFilteredList: operation matches: %#v\n", opv)
var actionName string
if opv.XOpenWhisk == nil {
actionName = ""
} else if len(opv.XOpenWhisk.Package) > 0 {
actionName = "/" + opv.XOpenWhisk.Namespace + "/" + opv.XOpenWhisk.Package + "/" + opv.XOpenWhisk.ActionName
} else {
actionName = "/" + opv.XOpenWhisk.Namespace + "/" + opv.XOpenWhisk.ActionName
}
orderInfo = AssignListInfo(actionName, op, apiName, basePath, path, baseUrl+path)
whisk.Debug(whisk.DbgInfo, "Appening to orderInfoArr: %s %s\n", orderInfo.RelPath)
orderInfoArr = append(orderInfoArr, orderInfo)
}
}
}
}
}
return orderInfoArr
}
// genFilteredRow(resultApi, api, maxApiNameSize, maxApiNameSize) generates an array of
// ApiFilteredRows for the purpose of ordering and printing in a list form by parsing and
// initializing vaules for each individual ApiFilteredRow struct.
// NOTE: Large action and api name values will be truncated by their associated max size parameters.
func genFilteredRow(resultApi *whisk.RetApi, apiPath string, apiVerb string, maxActionNameSize int, maxApiNameSize int) []whisk.ApiFilteredRow {
var orderInfo whisk.ApiFilteredRow
var orderInfoArr []whisk.ApiFilteredRow
baseUrl := strings.TrimSuffix(resultApi.BaseUrl, "/")
apiName := resultApi.Swagger.Info.Title
basePath := resultApi.Swagger.BasePath
if resultApi.Swagger != nil && resultApi.Swagger.Paths != nil {
for path := range resultApi.Swagger.Paths {
whisk.Debug(whisk.DbgInfo, "genFilteredRow: comparing api relpath: '%s'\n", path)
if len(apiPath) == 0 || path == apiPath {
whisk.Debug(whisk.DbgInfo, "genFilteredRow: relpath matches\n")
for op, opv := range resultApi.Swagger.Paths[path].MakeOperationMap() {
whisk.Debug(whisk.DbgInfo, "genFilteredRow: comparing operation: '%s'\n", op)
if len(apiVerb) == 0 || strings.ToLower(op) == strings.ToLower(apiVerb) {
whisk.Debug(whisk.DbgInfo, "genFilteredRow: operation matches: %#v\n", opv)
var actionName string
if opv.XOpenWhisk == nil {
actionName = ""
} else if len(opv.XOpenWhisk.Package) > 0 {
actionName = "/" + opv.XOpenWhisk.Namespace + "/" + opv.XOpenWhisk.Package + "/" + opv.XOpenWhisk.ActionName
} else {
actionName = "/" + opv.XOpenWhisk.Namespace + "/" + opv.XOpenWhisk.ActionName
}
orderInfo = AssignRowInfo(actionName[0:min(len(actionName), maxActionNameSize)], op, apiName[0:min(len(apiName), maxApiNameSize)], basePath, path, baseUrl+path)
orderInfo.FmtString = fmtString
whisk.Debug(whisk.DbgInfo, "Appening to orderInfoArr: %s %s\n", orderInfo.RelPath)
orderInfoArr = append(orderInfoArr, orderInfo)
}
}
}
}
}
return orderInfoArr
}
// AssignRowInfo(actionName, verb, apiName, basePath, relPath, url) assigns
// the given vaules to and initializes an ApiFilteredRow struct, then returns it.
func AssignRowInfo(actionName string, verb string, apiName string, basePath string, relPath string, url string) whisk.ApiFilteredRow {
var orderInfo whisk.ApiFilteredRow
orderInfo.ActionName = actionName
orderInfo.Verb = verb
orderInfo.ApiName = apiName
orderInfo.BasePath = basePath
orderInfo.RelPath = relPath
orderInfo.Url = url
return orderInfo
}
// AssignListInfo(actionName, verb, apiName, basePath, relPath, url) assigns
// the given vaules to and initializes an ApiFilteredList struct, then returns it.
func AssignListInfo(actionName string, verb string, apiName string, basePath string, relPath string, url string) whisk.ApiFilteredList {
var orderInfo whisk.ApiFilteredList
orderInfo.ActionName = actionName
orderInfo.Verb = verb
orderInfo.ApiName = apiName
orderInfo.BasePath = basePath
orderInfo.RelPath = relPath
orderInfo.Url = url
return orderInfo
}
func getLargestActionNameSize(retApiArray *whisk.RetApiArray, apiPath string, apiVerb string) int {
var maxNameSize = 0
for i := 0; i < len(retApiArray.Apis); i++ {
var resultApi = retApiArray.Apis[i].ApiValue
if resultApi.Swagger != nil && resultApi.Swagger.Paths != nil {
for path := range resultApi.Swagger.Paths {
whisk.Debug(whisk.DbgInfo, "getLargestActionNameSize: comparing api relpath: '%s'\n", path)
if len(apiPath) == 0 || path == apiPath {
whisk.Debug(whisk.DbgInfo, "getLargestActionNameSize: relpath matches\n")
for op, opv := range resultApi.Swagger.Paths[path].MakeOperationMap() {
whisk.Debug(whisk.DbgInfo, "getLargestActionNameSize: comparing operation: '%s'\n", op)
if len(apiVerb) == 0 || strings.ToLower(op) == strings.ToLower(apiVerb) {
whisk.Debug(whisk.DbgInfo, "getLargestActionNameSize: operation matches: %#v\n", opv)
var fullActionName string
if opv.XOpenWhisk == nil {
fullActionName = ""
} else if len(opv.XOpenWhisk.Package) > 0 {
fullActionName = "/" + opv.XOpenWhisk.Namespace + "/" + opv.XOpenWhisk.Package + "/" + opv.XOpenWhisk.ActionName
} else {
fullActionName = "/" + opv.XOpenWhisk.Namespace + "/" + opv.XOpenWhisk.ActionName
}
if len(fullActionName) > maxNameSize {
maxNameSize = len(fullActionName)
}
}
}
}
}
}
}
return maxNameSize
}
func getLargestApiNameSize(retApiArray *whisk.RetApiArray, apiPath string, apiVerb string) int {
var maxNameSize = 0
for i := 0; i < len(retApiArray.Apis); i++ {
var resultApi = retApiArray.Apis[i].ApiValue
apiName := resultApi.Swagger.Info.Title
if resultApi.Swagger != nil && resultApi.Swagger.Paths != nil {
for path := range resultApi.Swagger.Paths {
whisk.Debug(whisk.DbgInfo, "getLargestActionNameSize: comparing api relpath: '%s'\n", path)
if len(apiPath) == 0 || path == apiPath {
whisk.Debug(whisk.DbgInfo, "getLargestActionNameSize: relpath matches\n")
for op, opv := range resultApi.Swagger.Paths[path].MakeOperationMap() {
whisk.Debug(whisk.DbgInfo, "getLargestActionNameSize: comparing operation: '%s'\n", op)
if len(apiVerb) == 0 || strings.ToLower(op) == strings.ToLower(apiVerb) {
whisk.Debug(whisk.DbgInfo, "getLargestActionNameSize: operation matches: %#v\n", opv)
if len(apiName) > maxNameSize {
maxNameSize = len(apiName)
}
}
}
}
}
}
}
return maxNameSize
}
func generatePathParameters(relativePath string) ([]whisk.ApiParameter, error) {
pathParams := []whisk.ApiParameter{}
pathParamNames, err := getPathParameterNames(relativePath)
if len(pathParamNames) > 0 && err == nil {
// Only create unique swagger entries
var uniqueParamNames []string
for _, name := range pathParamNames {
if !contains(uniqueParamNames, name) {
uniqueParamNames = append(uniqueParamNames, name)
}
}
for _, uniqueName := range uniqueParamNames {
whisk.Debug(whisk.DbgInfo, "Creating api parameter for '%s'\n", uniqueName)
param := whisk.ApiParameter{Name: uniqueName, In: "path", Required: true, Type: "string",
Description: wski18n.T("Default description for '{{.name}}'", map[string]interface{}{"name": uniqueName})}
pathParams = append(pathParams, param)
}
}
return pathParams, err
}
/*
* if # args = 4
* args[0] = API base path
* args[0] = API relative path
* args[1] = API verb
* args[2] = Optional. Action name (may or may not be qualified with namespace and package name)
*
* if # args = 3
* args[0] = API relative path
* args[1] = API verb
* args[2] = Optional. Action name (may or may not be qualified with namespace and package name)
*/
func parseApi(cmd *cobra.Command, args []string) (*whisk.Api, *QualifiedName, error) {
var err error
var basepath string = "/"
var apiname string
var basepathArgIsApiName = false
api := new(whisk.Api)
if len(args) > 3 {
// Is the argument a basepath (must start with /) or an API name
if _, ok := isValidBasepath(args[0]); !ok {
whisk.Debug(whisk.DbgInfo, "Treating '%s' as an API name; as it does not begin with '/'\n", args[0])
basepathArgIsApiName = true
}
if err, _ := isBasepathParameterized(args[0]); err != nil {
return nil, nil, err
}
basepath = args[0]
// Shift the args so the remaining code works with or without the explicit base path arg
args = args[1:]
}
// Is the API path valid?
if len(args) > 0 {
if whiskErr, ok := isValidRelpath(args[0]); !ok {
return nil, nil, whiskErr
}
api.GatewayRelPath = args[0] // Maintain case as URLs may be case-sensitive
}
// Attempting to use path parameters, lets validate that they provided them correctly.
hasPathParams, err := hasPathParameters(api.GatewayRelPath)
if err != nil {
return nil, nil, err
}
// If they provided path Parameters, the response type better be http as its the only one that supports path parameters right now.
if hasPathParams && Flags.api.resptype != "http" {
errMsg := wski18n.T("A response type of 'http' is required when using path parameters.")
whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, nil, whiskErr
}
// Is the API verb valid?
if len(args) > 1 {
if whiskErr, ok := IsValidApiVerb(args[1]); !ok {
return nil, nil, whiskErr
}
api.GatewayMethod = strings.ToUpper(args[1])
}
// Is the specified action name valid?
var qName = new(QualifiedName)
if len(args) == 3 {
qName, err = NewQualifiedName(args[2])
if err != nil {
whisk.Debug(whisk.DbgError, "NewQualifiedName(%s) failed: %s\n", args[2], err)
errMsg := wski18n.T("'{{.name}}' is not a valid action name: {{.err}}",
map[string]interface{}{"name": args[2], "err": err})
whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, nil, whiskErr
}
if qName.GetEntityName() == "" {
whisk.Debug(whisk.DbgError, "Action name '%s' is invalid\n", args[2])
errMsg := wski18n.T("'{{.name}}' is not a valid action name.", map[string]interface{}{"name": args[2]})
whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, nil, whiskErr
}
}
if len(Flags.api.apiname) > 0 {
if basepathArgIsApiName {
// Specifying API name as argument AND as a --apiname option value is invalid
whisk.Debug(whisk.DbgError, "API is specified as an argument '%s' and as a flag '%s'\n", basepath, Flags.api.apiname)
errMsg := wski18n.T("An API name can only be specified once.")
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, nil, whiskErr
}
apiname = Flags.api.apiname
}
api.Namespace = Client.Config.Namespace
api.Action = new(whisk.ApiAction)
var urlActionPackage string
if len(qName.GetPackageName()) > 0 {
urlActionPackage = qName.GetPackageName()
} else {
urlActionPackage = "default"
}
backendUrl := Client.Config.Host + "/api/v1/web/" + qName.GetNamespace() + "/" + urlActionPackage + "/" + qName.GetEntity() + ".http"
if !strings.HasPrefix(backendUrl, "http") {
backendUrl = "https://" + backendUrl
}
api.Action.BackendUrl = backendUrl
api.Action.BackendMethod = api.GatewayMethod
api.Action.Name = qName.GetEntityName()
api.Action.Namespace = qName.GetNamespace()
api.Action.Auth = Client.Config.AuthToken
api.ApiName = apiname
api.GatewayBasePath = basepath
if !basepathArgIsApiName {
api.Id = "API:" + api.Namespace + ":" + api.GatewayBasePath
}
api.PathParameters, err = generatePathParameters(api.GatewayRelPath)
whisk.Debug(whisk.DbgInfo, "Parsed api struct: %#v\n", api)
return api, qName, err
}
func parseSwaggerApi(configfile string, namespace string) (*whisk.Api, error) {
// Test is for completeness, but this situation should only arise due to an internal error
if len(configfile) == 0 {
whisk.Debug(whisk.DbgError, "No swagger file is specified\n")
errMsg := wski18n.T("A configuration file was not specified.")
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, whiskErr
}
swagger, err := ReadFile(configfile)
if err != nil {
whisk.Debug(whisk.DbgError, "readFile(%s) error: %s\n", configfile, err)
errMsg := wski18n.T("Error reading swagger file '{{.name}}': {{.err}}",
map[string]interface{}{"name": configfile, "err": err})
whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, whiskErr
}
// Check if this swagger is in JSON or YAML format
isYaml := strings.HasSuffix(configfile, yamlFileExtension) || strings.HasSuffix(configfile, ymlFileExtension)
if isYaml {
whisk.Debug(whisk.DbgInfo, "Converting YAML formated API configuration into JSON\n")
jsonbytes, err := yaml.YAMLToJSON([]byte(swagger))
if err != nil {
whisk.Debug(whisk.DbgError, "yaml.YAMLToJSON() error: %s\n", err)
errMsg := wski18n.T("Unable to parse YAML configuration file: {{.err}}", map[string]interface{}{"err": err})
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
return nil, whiskErr
}
swagger = string(jsonbytes)
}
// Parse the JSON into a swagger object
swaggerObj := new(whisk.ApiSwagger)
err = json.Unmarshal([]byte(swagger), swaggerObj)
if err != nil {
whisk.Debug(whisk.DbgError, "JSON parse of '%s' error: %s\n", configfile, err)
errMsg := wski18n.T("Error parsing swagger file '{{.name}}': {{.err}}",
map[string]interface{}{"name": configfile, "err": err})
whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, whiskErr
}
if swaggerObj.BasePath == "" || swaggerObj.SwaggerName == "" || swaggerObj.Info == nil || swaggerObj.Paths == nil {
whisk.Debug(whisk.DbgError, "Swagger file is invalid.\n", configfile, err)
errMsg := wski18n.T("Swagger file is invalid (missing basePath, info, paths, or swagger fields)")
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, whiskErr
}
if _, ok := isValidBasepath(swaggerObj.BasePath); !ok {
whisk.Debug(whisk.DbgError, "Swagger file basePath is invalid.\n", configfile, err)
errMsg := wski18n.T("Swagger file basePath must start with a leading slash (/)")
whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL,
whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE)
return nil, whiskErr
}
api := new(whisk.Api)
api.Namespace = namespace
api.Swagger = swagger
return api, nil
}
func getAccessToken() (string, error) {
var token string = "DUMMY TOKEN"
var err error
props, err := ReadProps(Properties.PropsFile)
if err == nil {
if len(props["APIGW_ACCESS_TOKEN"]) > 0 {
token = props["APIGW_ACCESS_TOKEN"]
}
} else {
whisk.Debug(whisk.DbgError, "readProps(%s) failed: %s\n", Properties.PropsFile, err)
errStr := wski18n.T("Unable to obtain the API Gateway access token from the properties file: {{.err}}", map[string]interface{}{"err": err})
err = whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
}
return token, err
}
func getUserContextId() (string, error) {
var guid string
var err error
props, err := ReadProps(Properties.PropsFile)
if err == nil {
if len(props["AUTH"]) > 0 {
guid = strings.Split(props["AUTH"], ":")[0]
} else {
whisk.Debug(whisk.DbgError, "AUTH property not set in properties file: '%s'\n", Properties.PropsFile)
errStr := wski18n.T("Authorization key is not configured (--auth is required)")
err = whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
}
} else {
whisk.Debug(whisk.DbgError, "readProps(%s) failed: %s\n", Properties.PropsFile, err)
errStr := wski18n.T("Unable to obtain the auth key from the properties file: {{.err}}", map[string]interface{}{"err": err})
err = whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE)
}
return guid, err
}
///////////
// Flags //
///////////
func init() {
apiCreateCmd.Flags().StringVarP(&Flags.api.apiname, "apiname", "n", "", wski18n.T("Friendly name of the API; ignored when CFG_FILE is specified (default BASE_PATH)"))
apiCreateCmd.Flags().StringVarP(&Flags.api.configfile, "config-file", "c", "", wski18n.T("`CFG_FILE` containing API configuration in swagger JSON format"))
apiCreateCmd.Flags().StringVar(&Flags.api.resptype, "response-type", "json", wski18n.T("Set the web action response `TYPE`. Possible values are html, http, json, text, svg"))
apiGetCmd.Flags().BoolVarP(&Flags.common.detail, "full", "f", false, wski18n.T("display full API configuration details"))
apiGetCmd.Flags().StringVarP(&Flags.common.format, "format", "", formatOptionJson, wski18n.T("Specify the API output `TYPE`, either json or yaml"))
apiListCmd.Flags().IntVarP(&Flags.common.skip, "skip", "s", 0, wski18n.T("exclude the first `SKIP` number of actions from the result"))
apiListCmd.Flags().IntVarP(&Flags.common.limit, "limit", "l", 30, wski18n.T("only return `LIMIT` number of actions from the collection"))
apiListCmd.Flags().BoolVarP(&Flags.common.nameSort, "name-sort", "n", false, wski18n.T("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"))
apiListCmd.Flags().BoolVarP(&Flags.common.full, "full", "f", false, wski18n.T("display full description of each API"))
apiCmd.AddCommand(
apiCreateCmd,
apiGetCmd,
apiDeleteCmd,
apiListCmd,
)
}