whisk/api.go (440 lines of code) (raw):

/* * 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 whisk import ( "errors" "fmt" "github.com/apache/openwhisk-client-go/wski18n" "net/http" "strings" ) type ApiService struct { client *Client } // wsk api create : Request, Response type ApiCreateRequest struct { ApiDoc *Api `json:"apidoc,omitempty"` } type ApiCreateRequestOptions ApiOptions type ApiCreateResponse RetApi // wsk api list : Request, Response type ApiListRequest struct { } type ApiListRequestOptions struct { ApiOptions Limit int `url:"limit"` Skip int `url:"skip"` Docs bool `url:"docs,omitempty"` } type ApiListResponse RetApiArray // wsk api get : Request, Response type ApiGetRequest struct { Api } type ApiGetRequestOptions ApiOptions type ApiGetResponse RetApiArray // wsk api delete : Request, Response type ApiDeleteRequest struct { Api } type ApiDeleteRequestOptions ApiOptions type ApiDeleteResponse struct{} type Api struct { Namespace string `json:"namespace,omitempty"` ApiName string `json:"apiName,omitempty"` GatewayBasePath string `json:"gatewayBasePath,omitempty"` GatewayRelPath string `json:"gatewayPath,omitempty"` GatewayMethod string `json:"gatewayMethod,omitempty"` Id string `json:"id,omitempty"` GatewayFullPath string `json:"gatewayFullPath,omitempty"` Swagger string `json:"swagger,omitempty"` Action *ApiAction `json:"action,omitempty"` PathParameters []ApiParameter `json:"pathParameters,omitempty"` } type ApiParameter struct { Name string `json:"name"` In string `json:"in"` Description string `json:"description,omitempty"` Required bool `json:"required,omitempty"` Type string `json:"type,omitempty"` Format string `json:"format,omitempty"` AllowEmptyValue bool `json:"allowEmptyValue,omitempty"` Items map[string]interface{} `json:"items,omitempty"` CollectionFormat string `json:"collectionFormat,omitempty"` Default interface{} `json:"default,omitempty"` Maximum int `json:"maximum,omitempty"` ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` Minimum int `json:"minimum,omitempty"` ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` MaxLength int `json:"maxLength,omitempty"` MinLength int `json:"minLength,omitempty"` Pattern string `json:"pattern,omitempty"` MaxItems int `json:"maxItems,omitempty"` MinItems int `json:"minItems,omitempty"` UniqueItems bool `json:"uniqueItems,omitempty"` MultipleOf int `json:"multipleOf,omitempty"` Enum interface{} `json:"enum,omitempty"` Ref string `json:"$ref,omitempty"` } type ApiAction struct { Name string `json:"name,omitempty"` Namespace string `json:"namespace,omitempty"` BackendMethod string `json:"backendMethod,omitempty"` BackendUrl string `json:"backendUrl,omitempty"` Auth string `json:"authkey,omitempty"` SecureKey interface{} `json:"secureKey,omitempty"` } type ApiOptions struct { ActionName string `url:"action,omitempty"` ApiBasePath string `url:"basepath,omitempty"` ApiRelPath string `url:"relpath,omitempty"` ApiVerb string `url:"operation,omitempty"` ApiName string `url:"apiname,omitempty"` SpaceGuid string `url:"spaceguid,omitempty"` AccessToken string `url:"accesstoken,omitempty"` ResponseType string `url:"responsetype,omitempty"` } type ApiUserAuth struct { SpaceGuid string `json:"spaceguid,omitempty"` AccessToken string `json:"accesstoken,omitempty"` } type RetApiArray struct { Apis []ApiItem `json:"apis,omitempty"` } type ApiItem struct { ApiId string `json:"id,omitempty"` QueryKey string `json:"key,omitempty"` ApiValue *RetApi `json:"value,omitempty"` } type RetApi struct { Namespace string `json:"namespace"` BaseUrl string `json:"gwApiUrl"` Activated bool `json:"gwApiActivated"` TenantId string `json:"tenantId"` Swagger *ApiSwagger `json:"apidoc,omitempty"` } type ApiSwagger struct { SwaggerName string `json:"swagger,omitempty"` BasePath string `json:"basePath,omitempty"` Info *ApiSwaggerInfo `json:"info,omitempty"` Paths map[string]*ApiSwaggerPath `json:"paths,omitempty"` SecurityDef interface{} `json:"securityDefinitions,omitempty"` Security interface{} `json:"security,omitempty"` XConfig interface{} `json:"x-ibm-configuration,omitempty"` XRateLimit interface{} `json:"x-ibm-rate-limit,omitempty"` } type ApiSwaggerPath struct { Get *ApiSwaggerOperation `json:"get,omitempty"` Put *ApiSwaggerOperation `json:"put,omitempty"` Post *ApiSwaggerOperation `json:"post,omitempty"` Delete *ApiSwaggerOperation `json:"delete,omitempty"` Options *ApiSwaggerOperation `json:"options,omitempty"` Head *ApiSwaggerOperation `json:"head,omitempty"` Patch *ApiSwaggerOperation `json:"patch,omitempty"` Parameters []ApiParameter `json:"parameters,omitempty"` } func (asp *ApiSwaggerPath) MakeOperationMap() map[string]*ApiSwaggerOperation { var opMap map[string]*ApiSwaggerOperation = make(map[string]*ApiSwaggerOperation) if asp.Get != nil { opMap["get"] = asp.Get } if asp.Put != nil { opMap["put"] = asp.Put } if asp.Post != nil { opMap["post"] = asp.Post } if asp.Delete != nil { opMap["delete"] = asp.Delete } if asp.Options != nil { opMap["options"] = asp.Options } if asp.Head != nil { opMap["head"] = asp.Head } if asp.Patch != nil { opMap["patch"] = asp.Patch } return opMap } type ApiSwaggerInfo struct { Title string `json:"title,omitempty"` Version string `json:"version,omitempty"` } type ApiSwaggerOperation struct { OperationId string `json:"operationId"` Parameters []ApiParameter `json:"parameters,omitempty"` Responses interface{} `json:"responses"` XOpenWhisk *ApiSwaggerOpXOpenWhisk `json:"x-openwhisk,omitempty"` } type ApiSwaggerOpXOpenWhisk struct { ActionName string `json:"action"` Namespace string `json:"namespace"` Package string `json:"package"` 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, "POST": true, "DELETE": true, "PATCH": true, "HEAD": true, "OPTIONS": true, } const ( Overwrite = true DoNotOverwrite = false ) ///////////////// // 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" routeUrl, err := addRouteOptions(route, apiListOptions) if err != nil { Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, apiListOptions, err) errMsg := wski18n.T("Unable to add route options '{{.options}}'", map[string]interface{}{"options": apiListOptions}) whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) return nil, nil, whiskErr } Debug(DbgInfo, "Api GET/list route with api options: %s\n", routeUrl) req, err := s.client.NewRequestUrl("GET", routeUrl, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired) if err != nil { Debug(DbgError, "http.NewRequestUrl(GET, %s, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson) error: '%s'\n", routeUrl, err) errMsg := wski18n.T("Unable to create HTTP request for GET '{{.route}}': {{.err}}", map[string]interface{}{"route": routeUrl, "err": err}) whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return nil, nil, whiskErr } apiArray := new(ApiListResponse) resp, err := s.client.Do(req, &apiArray, ExitWithErrorOnTimeout) if err != nil { Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err) return nil, resp, err } err = validateApiListResponse(apiArray) if err != nil { Debug(DbgError, "Not a valid ApiListResponse object\n") return nil, resp, err } return apiArray, resp, err } func (s *ApiService) Insert(api *ApiCreateRequest, options *ApiCreateRequestOptions, overwrite bool) (*ApiCreateResponse, *http.Response, error) { route := "web/whisk.system/apimgmt/createApi.http" Debug(DbgInfo, "Api PUT route: %s\n", route) routeUrl, err := addRouteOptions(route, options) if err != nil { Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, options, err) errMsg := wski18n.T("Unable to add route options '{{.options}}'", map[string]interface{}{"options": options}) whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) return nil, nil, whiskErr } Debug(DbgError, "Api create route with options: %s\n", routeUrl) req, err := s.client.NewRequestUrl("POST", routeUrl, api, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired) if err != nil { Debug(DbgError, "http.NewRequestUrl(POST, %s, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson) error: '%s'\n", route, err) errMsg := wski18n.T("Unable to create HTTP request for POST '{{.route}}': {{.err}}", map[string]interface{}{"route": route, "err": err}) whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return nil, nil, whiskErr } retApi := new(ApiCreateResponse) resp, err := s.client.Do(req, &retApi, ExitWithErrorOnTimeout) if err != nil { Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err) return nil, resp, err } err = validateApiSwaggerResponse(retApi.Swagger) if err != nil { Debug(DbgError, "Not a valid API creation response\n") return nil, resp, err } return retApi, resp, nil } func (s *ApiService) Get(api *ApiGetRequest, options *ApiGetRequestOptions) (*ApiGetResponse, *http.Response, error) { route := "web/whisk.system/apimgmt/getApi.http" Debug(DbgInfo, "Api GET route: %s\n", route) routeUrl, err := addRouteOptions(route, options) if err != nil { Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, options, err) errMsg := wski18n.T("Unable to add route options '{{.options}}'", map[string]interface{}{"options": options}) whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) return nil, nil, whiskErr } Debug(DbgError, "Api get route with options: %s\n", routeUrl) req, err := s.client.NewRequestUrl("GET", routeUrl, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired) if err != nil { Debug(DbgError, "http.NewRequestUrl(GET, %s, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson) error: '%s'\n", route, err) errMsg := wski18n.T("Unable to create HTTP request for GET '{{.route}}': {{.err}}", map[string]interface{}{"route": route, "err": err}) whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return nil, nil, whiskErr } retApi := new(ApiGetResponse) resp, err := s.client.Do(req, &retApi, ExitWithErrorOnTimeout) if err != nil { Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err) return nil, resp, err } return retApi, resp, nil } func (s *ApiService) Delete(api *ApiDeleteRequest, options *ApiDeleteRequestOptions) (*http.Response, error) { route := "web/whisk.system/apimgmt/deleteApi.http" Debug(DbgInfo, "Api DELETE route: %s\n", route) routeUrl, err := addRouteOptions(route, options) if err != nil { Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, options, err) errMsg := wski18n.T("Unable to add route options '{{.options}}'", map[string]interface{}{"options": options}) whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) return nil, whiskErr } Debug(DbgError, "Api DELETE route with options: %s\n", routeUrl) req, err := s.client.NewRequestUrl("DELETE", routeUrl, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired) if err != nil { Debug(DbgError, "http.NewRequestUrl(DELETE, %s, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson) error: '%s'\n", route, err) errMsg := wski18n.T("Unable to create HTTP request for DELETE '{{.route}}': {{.err}}", map[string]interface{}{"route": route, "err": err}) whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return nil, whiskErr } retApi := new(ApiDeleteResponse) resp, err := s.client.Do(req, &retApi, ExitWithErrorOnTimeout) if err != nil { Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err) return resp, err } return nil, nil } func validateApiListResponse(apiList *ApiListResponse) error { for i := 0; i < len(apiList.Apis); i++ { if apiList.Apis[i].ApiValue == nil { Debug(DbgError, "validateApiResponse: No value stanza in api %v\n", apiList.Apis[i]) errMsg := wski18n.T("Internal error. Missing value stanza in API configuration response") whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return whiskErr } err := validateApiSwaggerResponse(apiList.Apis[i].ApiValue.Swagger) if err != nil { Debug(DbgError, "validateApiListResponse: Invalid Api: %v\n", apiList.Apis[i]) return err } } return nil } func validateApiSwaggerResponse(swagger *ApiSwagger) error { if swagger == nil { Debug(DbgError, "validateApiSwaggerResponse: No apidoc stanza in api\n") errMsg := wski18n.T("Internal error. Missing apidoc stanza in API configuration") whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return whiskErr } for path := range swagger.Paths { err := validateApiPath(swagger.Paths[path]) if err != nil { Debug(DbgError, "validateApiResponse: Invalid Api Path object: %v\n", swagger.Paths[path]) return err } } return nil } func validateApiPath(path *ApiSwaggerPath) error { for op, opv := range path.MakeOperationMap() { err := validateApiOperation(op, opv) if err != nil { Debug(DbgError, "validateApiPath: Invalid Api operation object: %v\n", opv) return err } } return nil } func validateApiOperation(opName string, op *ApiSwaggerOperation) error { if op.XOpenWhisk != nil && len(op.OperationId) == 0 { Debug(DbgError, "validateApiOperation: No operationId field in operation %v\n", op) errMsg := wski18n.T("Missing operationId field in API configuration for operation {{.op}}", map[string]interface{}{"op": opName}) whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return whiskErr } if op.XOpenWhisk != nil && len(op.XOpenWhisk.Namespace) == 0 { Debug(DbgError, "validateApiOperation: no x-openwhisk.namespace stanza in operation %v\n", op) errMsg := wski18n.T("Missing x-openwhisk.namespace field in API configuration for operation {{.op}}", map[string]interface{}{"op": opName}) whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return whiskErr } // Note: The op.XOpenWhisk.Package field can have a value of "", so don't enforce a value if op.XOpenWhisk != nil && len(op.XOpenWhisk.ActionName) == 0 { Debug(DbgError, "validateApiOperation: no x-openwhisk.action stanza in operation %v\n", op) errMsg := wski18n.T("Missing x-openwhisk.action field in API configuration for operation {{.op}}", map[string]interface{}{"op": opName}) whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return whiskErr } if op.XOpenWhisk != nil && len(op.XOpenWhisk.ApiUrl) == 0 { Debug(DbgError, "validateApiOperation: no x-openwhisk.url stanza in operation %v\n", op) errMsg := wski18n.T("Missing x-openwhisk.url field in API configuration for operation {{.op}}", map[string]interface{}{"op": opName}) whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) return whiskErr } return nil }