| /* |
| * 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 ( |
| "bytes" |
| "crypto/tls" |
| "encoding/base64" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "github.com/apache/incubator-openwhisk-client-go/wski18n" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "net/url" |
| "reflect" |
| "regexp" |
| "strings" |
| "time" |
| ) |
| |
| const ( |
| AuthRequired = true |
| NoAuth = false |
| IncludeNamespaceInUrl = true |
| DoNotIncludeNamespaceInUrl = false |
| AppendOpenWhiskPathPrefix = true |
| DoNotAppendOpenWhiskPathPrefix = false |
| EncodeBodyAsJson = "json" |
| EncodeBodyAsFormData = "formdata" |
| ProcessTimeOut = true |
| DoNotProcessTimeOut = false |
| ExitWithErrorOnTimeout = true |
| ExitWithSuccessOnTimeout = false |
| DEFAULT_HTTP_TIMEOUT = 30 |
| ) |
| |
| type Client struct { |
| client *http.Client |
| *Config |
| Transport *http.Transport |
| |
| Sdks *SdkService |
| Triggers *TriggerService |
| Actions *ActionService |
| Rules *RuleService |
| Activations *ActivationService |
| Packages *PackageService |
| Namespaces *NamespaceService |
| Info *InfoService |
| Apis *ApiService |
| } |
| |
| type Config struct { |
| Namespace string // NOTE :: Default is "_" |
| Cert string |
| Key string |
| AuthToken string |
| Host string |
| BaseURL *url.URL // NOTE :: Default is "openwhisk.ng.bluemix.net" |
| Version string |
| Verbose bool |
| Debug bool // For detailed tracing |
| Insecure bool |
| UserAgent string |
| ApigwAccessToken string |
| } |
| |
| type ObfuscateSet struct { |
| Regex string |
| Replacement string |
| } |
| |
| var DefaultObfuscateArr = []ObfuscateSet{ |
| { |
| Regex: "\"[Pp]assword\":\\s*\".*\"", |
| Replacement: `"password": "******"`, |
| }, |
| } |
| |
| func NewClient(httpClient *http.Client, config_input *Config) (*Client, error) { |
| |
| var config *Config |
| if config_input == nil { |
| defaultConfig, err := GetDefaultConfig() |
| if err != nil { |
| return nil, err |
| } else { |
| config = defaultConfig |
| } |
| } else { |
| config = config_input |
| } |
| |
| if httpClient == nil { |
| httpClient = &http.Client{ |
| Timeout: time.Second * DEFAULT_HTTP_TIMEOUT, |
| } |
| } |
| |
| var err error |
| var errStr = "" |
| if len(config.Host) == 0 { |
| errStr = wski18n.T("Unable to create request URL, because OpenWhisk API host is missing") |
| } else if config.BaseURL == nil { |
| config.BaseURL, err = GetUrlBase(config.Host) |
| if err != nil { |
| Debug(DbgError, "Unable to create request URL, because the api host %s is invalid\n", config.Host, err) |
| errStr = wski18n.T("Unable to create request URL, because the api host '{{.host}}' is invalid: {{.err}}", |
| map[string]interface{}{"host": config.Host, "err": err}) |
| } |
| } |
| |
| if len(errStr) != 0 { |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| |
| if len(config.Namespace) == 0 { |
| config.Namespace = "_" |
| } |
| |
| if len(config.Version) == 0 { |
| config.Version = "v1" |
| } |
| |
| if len(config.UserAgent) == 0 { |
| config.UserAgent = "OpenWhisk-Go-Client" |
| } |
| |
| c := &Client{ |
| client: httpClient, |
| Config: config, |
| } |
| |
| c.Sdks = &SdkService{client: c} |
| c.Triggers = &TriggerService{client: c} |
| c.Actions = &ActionService{client: c} |
| c.Rules = &RuleService{client: c} |
| c.Activations = &ActivationService{client: c} |
| c.Packages = &PackageService{client: c} |
| c.Namespaces = &NamespaceService{client: c} |
| c.Info = &InfoService{client: c} |
| c.Apis = &ApiService{client: c} |
| |
| return c, nil |
| } |
| |
| func (c *Client) LoadX509KeyPair() error { |
| tlsConfig := &tls.Config{ |
| InsecureSkipVerify: c.Config.Insecure, |
| } |
| |
| if c.Config.Cert != "" && c.Config.Key != "" { |
| if cert, err := ReadX509KeyPair(c.Config.Cert, c.Config.Key); err == nil { |
| tlsConfig.Certificates = []tls.Certificate{cert} |
| } else { |
| errStr := wski18n.T("Unable to load the X509 key pair due to the following reason: {{.err}}", |
| map[string]interface{}{"err": err}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return werr |
| } |
| } else if !c.Config.Insecure { |
| if c.Config.Cert == "" { |
| warningStr := "The Cert file is not configured. Please configure the missing Cert file, if there is a security issue accessing the service.\n" |
| Debug(DbgWarn, warningStr) |
| if c.Config.Key != "" { |
| errStr := wski18n.T("The Cert file is not configured. Please configure the missing Cert file.\n") |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return werr |
| } |
| } |
| if c.Config.Key == "" { |
| warningStr := "The Key file is not configured. Please configure the missing Key file, if there is a security issue accessing the service.\n" |
| Debug(DbgWarn, warningStr) |
| if c.Config.Cert != "" { |
| errStr := wski18n.T("The Key file is not configured. Please configure the missing Key file.\n") |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return werr |
| } |
| } |
| } |
| |
| // Use the defaultTransport as the transport basis to maintain proxy support |
| // Make a copy of the defaultTransport so that the original defaultTransport is left alone |
| defaultTransportCopy := *(http.DefaultTransport.(*http.Transport)) |
| defaultTransportCopy.TLSClientConfig = tlsConfig |
| c.client.Transport = &defaultTransportCopy |
| |
| return nil |
| } |
| |
| var ReadX509KeyPair = func(certFile, keyFile string) (tls.Certificate, error) { |
| return tls.LoadX509KeyPair(certFile, keyFile) |
| } |
| |
| /////////////////////////////// |
| // Request/Utility Functions // |
| /////////////////////////////// |
| |
| func (c *Client) NewRequest(method, urlStr string, body interface{}, includeNamespaceInUrl bool) (*http.Request, error) { |
| werr := c.LoadX509KeyPair() |
| if werr != nil { |
| return nil, werr |
| } |
| |
| if includeNamespaceInUrl { |
| if c.Config.Namespace != "" { |
| urlStr = fmt.Sprintf("%s/namespaces/%s/%s", c.Config.Version, c.Config.Namespace, urlStr) |
| } else { |
| urlStr = fmt.Sprintf("%s/namespaces", c.Config.Version) |
| } |
| } else { |
| urlStr = fmt.Sprintf("%s/%s", c.Config.Version, urlStr) |
| } |
| |
| urlStr = fmt.Sprintf("%s/%s", c.BaseURL.String(), urlStr) |
| u, err := url.Parse(urlStr) |
| if err != nil { |
| Debug(DbgError, "url.Parse(%s) error: %s\n", urlStr, err) |
| errStr := wski18n.T("Invalid request URL '{{.url}}': {{.err}}", |
| map[string]interface{}{"url": urlStr, "err": err}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| |
| var buf io.ReadWriter |
| if body != nil { |
| buf = new(bytes.Buffer) |
| encoder := json.NewEncoder(buf) |
| encoder.SetEscapeHTML(false) |
| err := encoder.Encode(body) |
| |
| if err != nil { |
| Debug(DbgError, "json.Encode(%#v) error: %s\n", body, err) |
| errStr := wski18n.T("Error encoding request body: {{.err}}", map[string]interface{}{"err": err}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| } |
| |
| req, err := http.NewRequest(method, u.String(), buf) |
| if err != nil { |
| Debug(DbgError, "http.NewRequest(%v, %s, buf) error: %s\n", method, u.String(), err) |
| errStr := wski18n.T("Error initializing request: {{.err}}", map[string]interface{}{"err": err}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| if req.Body != nil { |
| req.Header.Add("Content-Type", "application/json") |
| } |
| |
| err = c.addAuthHeader(req, AuthRequired) |
| if err != nil { |
| Debug(DbgError, "addAuthHeader() error: %s\n", err) |
| errStr := wski18n.T("Unable to add the HTTP authentication header: {{.err}}", |
| map[string]interface{}{"err": err}) |
| werr := MakeWskErrorFromWskError(errors.New(errStr), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| |
| req.Header.Add("User-Agent", c.Config.UserAgent) |
| |
| return req, nil |
| } |
| |
| func (c *Client) addAuthHeader(req *http.Request, authRequired bool) error { |
| if c.Config.AuthToken != "" { |
| encodedAuthToken := base64.StdEncoding.EncodeToString([]byte(c.Config.AuthToken)) |
| req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedAuthToken)) |
| Debug(DbgInfo, "Adding basic auth header; using authkey\n") |
| } else { |
| if authRequired { |
| Debug(DbgError, "The required authorization key is not configured - neither set as a property nor set via the --auth CLI argument\n") |
| errStr := wski18n.T("Authorization key is not configured (--auth is required)") |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_USAGE, DISPLAY_MSG, DISPLAY_USAGE) |
| return werr |
| } |
| } |
| return nil |
| } |
| |
| // bodyTruncator limits the size of Req/Resp Body for --verbose ONLY. |
| // It returns truncated Req/Resp Body, reloaded io.ReadCloser and any errors. |
| func BodyTruncator(body io.ReadCloser) (string, io.ReadCloser, error) { |
| limit := 1000 // 1000 byte limit, anything over is truncated |
| |
| data, err := ioutil.ReadAll(body) |
| if err != nil { |
| Verbose("ioutil.ReadAll(req.Body) error: %s\n", err) |
| werr := MakeWskError(err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return "", body, werr |
| } |
| |
| reload := ioutil.NopCloser(bytes.NewBuffer(data)) |
| |
| if len(data) > limit { |
| Verbose("Body exceeds %d bytes and will be truncated\n", limit) |
| newData := string(data)[:limit] + "..." |
| return string(newData), reload, nil |
| } |
| |
| return string(data), reload, nil |
| } |
| |
| // Do sends an API request and returns the API response. The API response is |
| // JSON decoded and stored in the value pointed to by v, or returned as an |
| // error if an API error has occurred. If v implements the io.Writer |
| // interface, the raw response body will be written to v, without attempting to |
| // first decode it. |
| func (c *Client) Do(req *http.Request, v interface{}, ExitWithErrorOnTimeout bool, secretToObfuscate ...ObfuscateSet) (*http.Response, error) { |
| var err error |
| var data []byte |
| secrets := append(DefaultObfuscateArr, secretToObfuscate...) |
| |
| req, err = PrintRequestInfo(req, secrets...) |
| //Putting this based on previous code |
| if err != nil { |
| return nil, err |
| } |
| |
| // Issue the request to the Whisk server endpoint |
| resp, err := c.client.Do(req) |
| if err != nil { |
| Debug(DbgError, "HTTP Do() [req %s] error: %s\n", req.URL.String(), err) |
| werr := MakeWskError(err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| |
| resp, data, err = PrintResponseInfo(resp, secrets...) |
| if err != nil { |
| return resp, err |
| } |
| |
| // With the HTTP response status code and the HTTP body contents, |
| // the possible response scenarios are: |
| // |
| // 0. HTTP Success + Body indicating a whisk failure result |
| // 1. HTTP Success + Valid body matching request expectations |
| // 2. HTTP Success + No body expected |
| // 3. HTTP Success + Body does NOT match request expectations |
| // 4. HTTP Failure + No body |
| // 5. HTTP Failure + Body matching error format expectation |
| // 6. HTTP Failure + Body NOT matching error format expectation |
| |
| // Handle 4. HTTP Failure + No body |
| // If this happens, just return no data and an error |
| if !IsHttpRespSuccess(resp) && data == nil { |
| Debug(DbgError, "HTTP failure %d + no body\n", resp.StatusCode) |
| werr := MakeWskError(errors.New(wski18n.T("Command failed due to an HTTP failure")), resp.StatusCode-256, |
| DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return resp, werr |
| } |
| |
| // Handle 5. HTTP Failure + Body matching error format expectation, or body matching a whisk.error() response |
| // Handle 6. HTTP Failure + Body NOT matching error format expectation |
| if !IsHttpRespSuccess(resp) && data != nil { |
| return parseErrorResponse(resp, data, v) |
| } |
| |
| // Handle 0. HTTP Success + Body indicating a whisk failure result |
| // NOTE: Need to ignore activation records send in response to 'wsk get activation NNN` as |
| // these will report the same original error giving the appearance that the command failed. |
| // Need to ignore `wsk action invoke NNN --result` too, otherwise action whose result is sth likes |
| // '{"response": {"key": "value"}}' will return an error to such command. |
| if IsHttpRespSuccess(resp) && // HTTP Status == 200 |
| data != nil && // HTTP response body exists |
| v != nil && |
| !strings.Contains(reflect.TypeOf(v).String(), "Activation") && // Request is not `wsk activation get` |
| !(req.URL.Query().Get("result") == "true") && // Request is not `wsk action invoke NNN --result` |
| !IsResponseResultSuccess(data) { // HTTP response body has Whisk error result |
| Debug(DbgInfo, "Got successful HTTP; but activation response reports an error\n") |
| return parseErrorResponse(resp, data, v) |
| } |
| |
| // Handle 2. HTTP Success + No body expected |
| if IsHttpRespSuccess(resp) && v == nil { |
| Debug(DbgInfo, "No interface provided; no HTTP response body expected\n") |
| return resp, nil |
| } |
| |
| // Handle 1. HTTP Success + Valid body matching request expectations |
| // Handle 3. HTTP Success + Body does NOT match request expectations |
| if IsHttpRespSuccess(resp) && v != nil { |
| |
| // If a timeout occurs, 202 HTTP status code is returned, and the caller wishes to handle such an event, return |
| // an error corresponding with the timeout |
| if ExitWithErrorOnTimeout && resp.StatusCode == EXIT_CODE_TIMED_OUT { |
| errMsg := wski18n.T("Request accepted, but processing not completed yet.") |
| err = MakeWskError(errors.New(errMsg), EXIT_CODE_TIMED_OUT, NO_DISPLAY_MSG, NO_DISPLAY_USAGE, |
| NO_MSG_DISPLAYED, NO_DISPLAY_PREFIX, NO_APPLICATION_ERR, TIMED_OUT) |
| } |
| |
| return parseSuccessResponse(resp, data, v), err |
| } |
| |
| // We should never get here, but just in case return failure to keep the compiler happy |
| werr := MakeWskError(errors.New(wski18n.T("Command failed due to an internal failure")), EXIT_CODE_ERR_GENERAL, |
| DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return resp, werr |
| } |
| |
| func PrintRequestInfo(req *http.Request, secretToObfuscate ...ObfuscateSet) (*http.Request, error) { |
| var truncatedBody string |
| var err error |
| if IsVerbose() { |
| fmt.Println("REQUEST:") |
| fmt.Printf("[%s]\t%s\n", req.Method, req.URL) |
| |
| if len(req.Header) > 0 { |
| fmt.Println("Req Headers") |
| PrintJSON(req.Header) |
| } |
| |
| if req.Body != nil { |
| fmt.Println("Req Body") |
| // Since we're emptying out the reader, which is the req.Body, we have to reset it, |
| // but create some copies for our debug messages. |
| buffer, _ := ioutil.ReadAll(req.Body) |
| obfuscatedRequest := ObfuscateText(string(buffer), secretToObfuscate) |
| req.Body = ioutil.NopCloser(bytes.NewBuffer(buffer)) |
| |
| if !IsDebug() { |
| if truncatedBody, req.Body, err = BodyTruncator(ioutil.NopCloser(bytes.NewBuffer(buffer))); err != nil { |
| return nil, err |
| } |
| fmt.Println(ObfuscateText(truncatedBody, secretToObfuscate)) |
| } else { |
| fmt.Println(obfuscatedRequest) |
| } |
| Debug(DbgInfo, "Req Body (ASCII quoted string):\n%+q\n", obfuscatedRequest) |
| } |
| } |
| return req, nil |
| } |
| |
| func PrintResponseInfo(resp *http.Response, secretToObfuscate ...ObfuscateSet) (*http.Response, []byte, error) { |
| var truncatedBody string |
| // Don't "defer resp.Body.Close()" here because the body is reloaded to allow caller to |
| // do custom body parsing, such as handling per-route error responses. |
| Verbose("RESPONSE:") |
| Verbose("Got response with code %d\n", resp.StatusCode) |
| |
| if IsVerbose() && len(resp.Header) > 0 { |
| fmt.Println("Resp Headers") |
| PrintJSON(resp.Header) |
| } |
| |
| // Read the response body |
| data, err := ioutil.ReadAll(resp.Body) |
| if err != nil { |
| Debug(DbgError, "ioutil.ReadAll(resp.Body) error: %s\n", err) |
| werr := MakeWskError(err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| resp.Body = ioutil.NopCloser(bytes.NewBuffer(data)) |
| return resp, data, werr |
| } |
| |
| // Reload the response body to allow caller access to the body; otherwise, |
| // the caller will have any empty body to read |
| resp.Body = ioutil.NopCloser(bytes.NewBuffer(data)) |
| |
| Verbose("Response body size is %d bytes\n", len(data)) |
| |
| if !IsDebug() { |
| if truncatedBody, resp.Body, err = BodyTruncator(ioutil.NopCloser(bytes.NewBuffer(data))); err != nil { |
| return nil, data, err |
| } |
| Verbose("Response body received:\n%s\n", ObfuscateText(truncatedBody, secretToObfuscate)) |
| } else { |
| obfuscatedResponse := ObfuscateText(string(data), secretToObfuscate) |
| Verbose("Response body received:\n%s\n", obfuscatedResponse) |
| Debug(DbgInfo, "Response body received (ASCII quoted string):\n%+q\n", obfuscatedResponse) |
| } |
| return resp, data, err |
| } |
| |
| func ObfuscateText(text string, replacements []ObfuscateSet) string { |
| obfuscated := text |
| for _, oSet := range replacements { |
| r, _ := regexp.Compile(oSet.Regex) |
| obfuscated = r.ReplaceAllString(obfuscated, oSet.Replacement) |
| } |
| return obfuscated |
| } |
| |
| func parseErrorResponse(resp *http.Response, data []byte, v interface{}) (*http.Response, error) { |
| Debug(DbgInfo, "HTTP failure %d + body\n", resp.StatusCode) |
| |
| // Determine if an application error was received (#5) |
| errorResponse := &ErrorResponse{Response: resp} |
| err := json.Unmarshal(data, errorResponse) |
| |
| // Determine if error is an application error or an error generated by API |
| if err == nil { |
| if errorResponse.Code == nil /*&& errorResponse.ErrMsg != nil */ && resp.StatusCode == 502 { |
| return parseApplicationError(resp, data, v) |
| } else if errorResponse.Code != nil && errorResponse.ErrMsg != nil { |
| Debug(DbgInfo, "HTTP failure %d; server error %s\n", resp.StatusCode, errorResponse) |
| werr := MakeWskError(errorResponse, resp.StatusCode-256, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return resp, werr |
| } |
| } |
| |
| // Body contents are unknown (#6) |
| Debug(DbgError, "HTTP response with unexpected body failed due to contents parsing error: '%v'\n", err) |
| errMsg := wski18n.T("The connection failed, or timed out. (HTTP status code {{.code}})", |
| map[string]interface{}{"code": resp.StatusCode}) |
| whiskErr := MakeWskError(errors.New(errMsg), resp.StatusCode-256, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return resp, whiskErr |
| } |
| |
| func parseApplicationError(resp *http.Response, data []byte, v interface{}) (*http.Response, error) { |
| Debug(DbgInfo, "Parsing application error\n") |
| |
| whiskErrorResponse := &WhiskErrorResponse{} |
| err := json.Unmarshal(data, whiskErrorResponse) |
| |
| // Handle application errors that occur when --result option is false (#5) |
| if err == nil && whiskErrorResponse != nil && whiskErrorResponse.Response != nil && whiskErrorResponse.Response.Status != nil { |
| Debug(DbgInfo, "Detected response status `%s` that a whisk.error(\"%#v\") was returned\n", |
| *whiskErrorResponse.Response.Status, *whiskErrorResponse.Response.Result) |
| errMsg := wski18n.T("The following application error was received: {{.err}}", |
| map[string]interface{}{"err": *whiskErrorResponse.Response.Result}) |
| whiskErr := MakeWskError(errors.New(errMsg), resp.StatusCode-256, NO_DISPLAY_MSG, NO_DISPLAY_USAGE, |
| NO_MSG_DISPLAYED, DISPLAY_PREFIX, APPLICATION_ERR) |
| return parseSuccessResponse(resp, data, v), whiskErr |
| } |
| |
| appErrResult := &AppErrorResult{} |
| err = json.Unmarshal(data, appErrResult) |
| |
| // Handle application errors that occur with blocking invocations when --result option is true (#5) |
| if err == nil && appErrResult.Error != nil { |
| Debug(DbgInfo, "Error code is null, blocking with result invocation error has occured\n") |
| errMsg := fmt.Sprintf("%v", *appErrResult.Error) |
| Debug(DbgInfo, "Application error received: %s\n", errMsg) |
| |
| whiskErr := MakeWskError(errors.New(errMsg), resp.StatusCode-256, NO_DISPLAY_MSG, NO_DISPLAY_USAGE, |
| NO_MSG_DISPLAYED, DISPLAY_PREFIX, APPLICATION_ERR) |
| return parseSuccessResponse(resp, data, v), whiskErr |
| } |
| |
| // Body contents are unknown (#6) |
| Debug(DbgError, "HTTP response with unexpected body failed due to contents parsing error: '%v'\n", err) |
| errMsg := wski18n.T("The connection failed, or timed out. (HTTP status code {{.code}})", |
| map[string]interface{}{"code": resp.StatusCode}) |
| whiskErr := MakeWskError(errors.New(errMsg), resp.StatusCode-256, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return resp, whiskErr |
| } |
| |
| func parseSuccessResponse(resp *http.Response, data []byte, v interface{}) *http.Response { |
| Debug(DbgInfo, "Parsing HTTP response into struct type: %s\n", reflect.TypeOf(v)) |
| |
| dc := json.NewDecoder(strings.NewReader(string(data))) |
| dc.UseNumber() |
| err := dc.Decode(v) |
| |
| // If the decode was successful, return the response without error (#1). Otherwise, the decode did not work, so the |
| // server response was unexpected (#3) |
| if err == nil { |
| Debug(DbgInfo, "Successful parse of HTTP response into struct type: %s\n", reflect.TypeOf(v)) |
| return resp |
| } else { |
| Debug(DbgWarn, "Unsuccessful parse of HTTP response into struct type: %s; parse error '%v'\n", reflect.TypeOf(v), err) |
| Debug(DbgWarn, "Request was successful, so ignoring the following unexpected response body that could not be parsed: %s\n", data) |
| return resp |
| } |
| } |
| |
| //////////// |
| // Errors // |
| //////////// |
| |
| // For containing the server response body when an error message is returned |
| // Here's an example error response body with HTTP status code == 400 |
| // { |
| // "error": "namespace contains invalid characters", |
| // "code": 1422870 |
| // } |
| type ErrorResponse struct { |
| Response *http.Response // HTTP response that caused this error |
| ErrMsg *interface{} `json:"error"` // error message string |
| Code *int64 `json:"code"` // validation error code |
| } |
| |
| type AppErrorResult struct { |
| Error *interface{} `json:"error"` |
| } |
| |
| type WhiskErrorResponse struct { |
| Response *WhiskResponse `json:"response"` |
| } |
| |
| type WhiskResponse struct { |
| Result *WhiskResult `json:"result"` |
| Success bool `json:"success"` |
| Status *interface{} `json:"status"` |
| } |
| |
| type WhiskResult struct { |
| // Error *WhiskError `json:"error"` // whisk.error(<string>) and whisk.reject({msg:<string>}) result in two different kinds of 'error' JSON objects |
| } |
| |
| type WhiskError struct { |
| Msg *string `json:"msg"` |
| } |
| |
| func (r ErrorResponse) Error() string { |
| return wski18n.T("{{.msg}} (code {{.code}})", |
| map[string]interface{}{"msg": fmt.Sprintf("%v", *r.ErrMsg), "code": r.Code}) |
| } |
| |
| //////////////////////////// |
| // Basic Client Functions // |
| //////////////////////////// |
| |
| func IsHttpRespSuccess(r *http.Response) bool { |
| return r.StatusCode >= 200 && r.StatusCode <= 299 |
| } |
| |
| func IsResponseResultSuccess(data []byte) bool { |
| errResp := new(WhiskErrorResponse) |
| err := json.Unmarshal(data, &errResp) |
| if err == nil && errResp.Response != nil { |
| return errResp.Response.Success |
| } |
| Debug(DbgWarn, "IsResponseResultSuccess: failed to parse response result: %v\n", err) |
| return true |
| } |
| |
| // |
| // Create a HTTP request object using URL stored in url.URL object |
| // Arguments: |
| // method - HTTP verb (i.e. "GET", "PUT", etc) |
| // urlRelResource - *url.URL structure representing the relative resource URL, including query params |
| // body - optional. Object whose contents will be JSON encoded and placed in HTTP request body |
| // includeNamespaceInUrl - when true "/namespaces/NAMESPACE" is included in the final URL; otherwise not included. |
| // appendOpenWhiskPath - when true, the OpenWhisk URL format is generated |
| // encodeBodyAs - specifies body encoding (json or form data) |
| // useAuthentication - when true, the basic Authorization is included with the configured authkey as the value |
| func (c *Client) NewRequestUrl( |
| method string, |
| urlRelResource *url.URL, |
| body interface{}, |
| includeNamespaceInUrl bool, |
| appendOpenWhiskPath bool, |
| encodeBodyAs string, |
| useAuthentication bool) (*http.Request, error) { |
| var requestUrl *url.URL |
| var err error |
| error := c.LoadX509KeyPair() |
| if error != nil { |
| return nil, error |
| } |
| |
| if appendOpenWhiskPath { |
| var urlVerNamespaceStr string |
| var verPathEncoded = (&url.URL{Path: c.Config.Version}).String() |
| |
| if includeNamespaceInUrl { |
| if c.Config.Namespace != "" { |
| // Encode path parts before inserting them into the URI so that any '?' is correctly encoded |
| // as part of the path and not the start of the query params |
| verNamespaceEncoded := (&url.URL{Path: c.Config.Namespace}).String() |
| urlVerNamespaceStr = fmt.Sprintf("%s/namespaces/%s", verPathEncoded, verNamespaceEncoded) |
| } else { |
| urlVerNamespaceStr = fmt.Sprintf("%s/namespaces", verPathEncoded) |
| } |
| } else { |
| urlVerNamespaceStr = fmt.Sprintf("%s", verPathEncoded) |
| } |
| |
| // Assemble the complete URL: base + version + [namespace] + resource_relative_path |
| Debug(DbgInfo, "basepath: %s, version/namespace path: %s, resource path: %s\n", c.BaseURL.String(), urlVerNamespaceStr, urlRelResource.String()) |
| urlStr := fmt.Sprintf("%s/%s/%s", c.BaseURL.String(), urlVerNamespaceStr, urlRelResource.String()) |
| requestUrl, err = url.Parse(urlStr) |
| if err != nil { |
| Debug(DbgError, "url.Parse(%s) error: %s\n", urlStr, err) |
| errStr := wski18n.T("Invalid request URL '{{.url}}': {{.err}}", |
| map[string]interface{}{"url": urlStr, "err": err}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| } else { |
| Debug(DbgInfo, "basepath: %s, resource path: %s\n", c.BaseURL.String(), urlRelResource.String()) |
| urlStr := fmt.Sprintf("%s/%s", c.BaseURL.String(), urlRelResource.String()) |
| requestUrl, err = url.Parse(urlStr) |
| if err != nil { |
| Debug(DbgError, "url.Parse(%s) error: %s\n", urlStr, err) |
| errStr := wski18n.T("Invalid request URL '{{.url}}': {{.err}}", |
| map[string]interface{}{"url": urlStr, "err": err}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| } |
| |
| var buf io.ReadWriter |
| if body != nil { |
| if encodeBodyAs == EncodeBodyAsJson { |
| buf = new(bytes.Buffer) |
| encoder := json.NewEncoder(buf) |
| encoder.SetEscapeHTML(false) |
| err := encoder.Encode(body) |
| |
| if err != nil { |
| Debug(DbgError, "json.Encode(%#v) error: %s\n", body, err) |
| errStr := wski18n.T("Error encoding request body: {{.err}}", |
| map[string]interface{}{"err": err}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| } else if encodeBodyAs == EncodeBodyAsFormData { |
| if values, ok := body.(url.Values); ok { |
| buf = bytes.NewBufferString(values.Encode()) |
| } else { |
| Debug(DbgError, "Invalid form data body: %v\n", body) |
| errStr := wski18n.T("Internal error. Form data encoding failure") |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| } else { |
| Debug(DbgError, "Invalid body encode type: %s\n", encodeBodyAs) |
| errStr := wski18n.T("Internal error. Invalid encoding type '{{.encodetype}}'", |
| map[string]interface{}{"encodetype": encodeBodyAs}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| } |
| |
| req, err := http.NewRequest(method, requestUrl.String(), buf) |
| if err != nil { |
| Debug(DbgError, "http.NewRequest(%v, %s, buf) error: %s\n", method, requestUrl.String(), err) |
| errStr := wski18n.T("Error initializing request: {{.err}}", map[string]interface{}{"err": err}) |
| werr := MakeWskError(errors.New(errStr), EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| if req.Body != nil && encodeBodyAs == EncodeBodyAsJson { |
| req.Header.Add("Content-Type", "application/json") |
| } |
| if req.Body != nil && encodeBodyAs == EncodeBodyAsFormData { |
| req.Header.Add("Content-Type", "application/x-www-form-urlencoded") |
| } |
| |
| if useAuthentication { |
| err = c.addAuthHeader(req, AuthRequired) |
| if err != nil { |
| Debug(DbgError, "addAuthHeader() error: %s\n", err) |
| errStr := wski18n.T("Unable to add the HTTP authentication header: {{.err}}", |
| map[string]interface{}{"err": err}) |
| werr := MakeWskErrorFromWskError(errors.New(errStr), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG, NO_DISPLAY_USAGE) |
| return nil, werr |
| } |
| } else { |
| Debug(DbgInfo, "No auth header required\n") |
| } |
| |
| req.Header.Add("User-Agent", c.Config.UserAgent) |
| |
| return req, nil |
| } |