blob: 94ab4478e4c67d32a8b49d38c99efe67796c3061 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more contributor
// license agreements; and to You under the Apache License, Version 2.0.
'use strict'
const messages = require('./messages')
const OpenWhiskError = require('./openwhisk_error')
const needle = require('needle')
const url = require('url')
const http = require('http')
/**
* This implements a request-promise-like facade over the needle
* library. There are two gaps between needle and rp that need to be
* bridged: 1) convert `qs` into a query string; and 2) convert
* needle's non-excepting >=400 statusCode responses into exceptions
*
*/
const rp = opts => {
if (opts.qs) {
// we turn the qs struct into a query string over the url
let first = true
for (let key in opts.qs) {
const str = `${encodeURIComponent(key)}=${encodeURIComponent(opts.qs[key])}`
if (first) {
opts.url += `?${str}`
first = false
} else {
opts.url += `&${str}`
}
}
}
// it appears that certain call paths from our code do not set the
// opts.json field to true; rp is apparently more resilient to
// this situation than needle
opts.json = true
return needle(opts.method.toLowerCase(), // needle takes e.g. 'put' not 'PUT'
opts.url,
opts.body || opts.params,
opts)
.then(resp => {
if (resp.statusCode >= 400) {
// we turn >=400 statusCode responses into exceptions
const error = new Error(resp.body.error || resp.statusMessage)
error.statusCode = resp.statusCode // the http status code
error.options = opts // the code below requires access to the input opts
error.error = resp.body // the error body
throw error
} else {
// otherwise, the response body is the expected return value
return resp.body
}
})
}
class Client {
/**
* @constructor
* @param {Object} options - options of the Client
* @param {string} [options.api]
* @param {string} [options.api_key]
* @param {string} [options.apihost]
* @param {string} [options.apiversion]
* @param {string} [options.namespace]
* @param {boolean} [options.ignore_certs]
* @param {string} [options.apigw_token]
* @param {string} [options.apigw_space_guid]
* @param {Function} [options.auth_handler]
* @param {boolean} [options.noUserAgent]
* @param {string} [options.cert]
* @param {string} [options.key]
*/
constructor (options) {
this.options = this.parseOptions(options || {})
}
parseOptions (options) {
const apiKey = options.api_key || process.env['__OW_API_KEY']
const ignoreCerts = options.ignore_certs ||
(process.env['__OW_IGNORE_CERTS']
? process.env['__OW_IGNORE_CERTS'].toLowerCase() === 'true'
: false)
// gather proxy settings if behind a firewall
const proxy = options.proxy || process.env.PROXY || process.env.proxy ||
process.env.HTTP_PROXY || process.env.http_proxy ||
process.env.HTTPS_PROXY || process.env.https_proxy
// custom HTTP agent
const agent = options.agent
// if apiversion is available, use it
const apiversion = options.apiversion || 'v1'
// if apihost is available, parse this into full API url
const api = options.api ||
this.urlFromApihost(options.apihost || process.env['__OW_API_HOST'], apiversion)
// optional tokens for API GW service
const apigwToken = options.apigw_token || process.env['__OW_APIGW_TOKEN']
let apigwSpaceGuid = options.apigw_space_guid || process.env['__OW_APIGW_SPACE_GUID']
// unless space is explicitly passed, default to using auth uuid.
if (apigwToken && !apigwSpaceGuid) {
apigwSpaceGuid = apiKey.split(':')[0]
}
if (!apiKey && !options.auth_handler) {
throw new Error(`${messages.INVALID_OPTIONS_ERROR} Missing api_key parameter or token plugin.`)
} else if (!api) {
throw new Error(`${messages.INVALID_OPTIONS_ERROR} Missing either api or apihost parameters.`)
}
return {apiKey: apiKey, api, apiVersion: apiversion, ignoreCerts: ignoreCerts, namespace: options.namespace, apigwToken: apigwToken, apigwSpaceGuid: apigwSpaceGuid, authHandler: options.auth_handler, noUserAgent: options.noUserAgent, cert: options.cert, key: options.key, proxy, agent}
}
urlFromApihost (apihost, apiversion = 'v1') {
if (!apihost) return apihost
let url = `${apihost}/api/${apiversion}/`
// if apihost does not the protocol, assume HTTPS
if (!url.match(/http(s)?:\/\//)) {
url = `https://${url}`
}
return url
}
request (method, path, options) {
const params = this.params(method, path, options)
return params.then(req => rp(req)).catch(err => this.handleErrors(err))
}
params (method, path, options) {
return this.authHeader().then(header => {
const parms = Object.assign({
json: true,
method: method,
url: this.pathUrl(path),
rejectUnauthorized: !this.options.ignoreCerts,
headers: {
'User-Agent': (options && options['User-Agent']) || 'openwhisk-client-js',
Authorization: header
}
}, options)
if (this.options.cert && this.options.key) {
parms.cert = this.options.cert
parms.key = this.options.key
}
if (this.options.noUserAgent || parms.noUserAgent) {
// caller asked for no user agent?
parms.headers['User-Agent'] = undefined
}
if (typeof this.options.namespace === 'string') {
// identify namespace targeting a public/shared entity
parms.headers['x-namespace-id'] = this.options.namespace
}
if (this.options.proxy) {
parms.proxy = this.options.proxy
}
if (this.options.agent) {
parms.agent = this.options.agent
}
return parms
})
}
pathUrl (urlPath) {
const endpoint = this.apiUrl()
endpoint.pathname = url.resolve(endpoint.pathname, urlPath)
return url.format(endpoint)
}
apiUrl () {
return url.parse(
this.options.api.endsWith('/') ? this.options.api : this.options.api + '/'
)
}
authHeader () {
if (this.options.authHandler) {
return this.options.authHandler.getAuthHeader()
} else {
const apiKeyBase64 = Buffer.from(this.options.apiKey).toString('base64')
return Promise.resolve(`Basic ${apiKeyBase64}`)
}
}
handleErrors (reason) {
let message = `Unknown Error From API: ${reason.message}`
if (reason.hasOwnProperty('statusCode')) {
const responseError = this.errMessage(reason.error)
message = `${reason.options.method} ${reason.options.url} Returned HTTP ${reason.statusCode} (${http.STATUS_CODES[reason.statusCode]}) --> "${responseError}"`
}
throw new OpenWhiskError(message, reason.error, reason.statusCode)
}
// Error messages might be returned from platform or using custom
// invocation result response from action.
errMessage (error) {
if (!error) return 'Response Missing Error Message.'
if (typeof error.error === 'string') {
return error.error
} else if (error.response && error.response.result) {
const result = error.response.result
if (result.error) {
if (typeof result.error === 'string') {
return result.error
} else if (typeof result.error.error === 'string') {
return result.error.error
} else if (result.error.statusCode) {
return `application error, status code: ${result.error.statusCode}`
}
}
}
return 'Response Missing Error Message.'
}
}
module.exports = Client