UNPKG

osls

Version:

Open-source alternative to Serverless Framework

251 lines (228 loc) 8.84 kB
'use strict'; const _ = require('lodash'); const memoize = require('memoizee'); const PromiseQueue = require('promise-queue'); const sdk = require('./sdk-v2'); const ServerlessError = require('../../lib/serverless-error'); const { log } = require('@serverless/utils/log'); const HttpsProxyAgent = require('https-proxy-agent'); const url = require('url'); const https = require('https'); const fs = require('fs'); const deepSortObjectByKey = require('../utils/deep-sort-object-by-key'); const ensureString = require('type/string/ensure'); const isObject = require('type/object/is'); const wait = require('timers-ext/promise/sleep'); const chalk = require('chalk'); // Activate AWS SDK logging const awsLog = log.get('aws'); // Use HTTPS Proxy (Optional) const proxy = process.env.proxy || process.env.HTTP_PROXY || process.env.http_proxy || process.env.HTTPS_PROXY || process.env.https_proxy; const proxyOptions = {}; if (proxy) { // not relying on recommended WHATWG URL // due to missing support for it in https-proxy-agent // https://github.com/TooTallNate/node-https-proxy-agent/issues/117 Object.assign(proxyOptions, url.parse(proxy)); } const ca = process.env.ca || process.env.HTTPS_CA || process.env.https_ca; let caCerts = []; if (ca) { // Can be a single certificate or multiple, comma separated. const caArr = ca.split(','); // Replace the newline -- https://stackoverflow.com/questions/30400341 caCerts = caCerts.concat(caArr.map((cert) => cert.replace(/\\n/g, '\n'))); } const cafile = process.env.cafile || process.env.HTTPS_CAFILE || process.env.https_cafile; if (cafile) { // Can be a single certificate file path or multiple paths, comma separated. const caPathArr = cafile.split(','); caCerts = caCerts.concat(caPathArr.map((cafilePath) => fs.readFileSync(cafilePath.trim()))); } if (caCerts.length > 0) { Object.assign(proxyOptions, { rejectUnauthorized: true, ca: caCerts, }); } // Passes also certifications if (proxy) { sdk.config.httpOptions.agent = new HttpsProxyAgent(proxyOptions); } else if (proxyOptions.ca) { // Update the agent -- http://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-registering-certs.html sdk.config.httpOptions.agent = new https.Agent(proxyOptions); } // Configure the AWS Client timeout (Optional). The default is 120000 (2 minutes) const timeout = process.env.AWS_CLIENT_TIMEOUT || process.env.aws_client_timeout; if (timeout) { sdk.config.httpOptions.timeout = parseInt(timeout, 10); } PromiseQueue.configure(Promise); const requestQueue = new PromiseQueue(2, Infinity); const MAX_RETRIES = (() => { const userValue = Number(process.env.SLS_AWS_REQUEST_MAX_RETRIES); return userValue >= 0 ? userValue : 4; })(); const accelerationCompatibleS3Methods = new Set(['upload', 'putObject']); const shouldS3Accelerate = (method, params) => { if ( accelerationCompatibleS3Methods.has(method) && params && params.isS3TransferAccelerationEnabled ) { log.notice('Using S3 Transfer Acceleration Endpoint...'); return true; } return false; }; const getServiceInstance = memoize( (service, method) => { const Service = _.get(sdk, service.name); // we translate params to an object for the service creation by selecting keys of interest const serviceParams = { ...service.params }; if (service.name === 'S3') { serviceParams.useAccelerateEndpoint = shouldS3Accelerate(method, service.params); } return new Service(serviceParams); }, { normalizer: ([service, method]) => { return [JSON.stringify(deepSortObjectByKey(service)), method].join('|'); }, } ); const normalizerPattern = /(?<!^)([A-Z])/g; const normalizeErrorCodePostfix = (name) => { return name.replace(normalizerPattern, '_$1').toUpperCase(); }; let requestCounter = 0; /** Execute request to AWS service * @param {Object|string} [service] - Description of the service to call * @prop [service.name] - Name of the service to call, support subclasses * @prop [service.params] - Parameters to apply when creating the service and doing the request * @prop [service.params.credentials] - AWS Credentials to use * @prop [service.params.useCache ] - Wether to reuse result of the same request cached locally * @prop [service.params.region] - Region in which the call should be made (default to us-east-1) * @prop [service.params.isS3TransferAccelerationEnabled] - Use s3 acceleration when available for the request * @param {String} method - Method to call * @param {Array} args - Argument for the method call */ async function awsRequest(service, method, ...args) { // Checks regarding expectations on service object if (isObject(service)) { ensureString(service.name, { name: 'service.name' }); } else { ensureString(service, { name: 'service' }); service = { name: service }; } const BASE_BACKOFF = 5000; const persistentRequest = async (f, numTry = 0) => { try { return await f(); } catch (e) { const { providerError } = e; if ( numTry < MAX_RETRIES && providerError && ((providerError.retryable && providerError.statusCode !== 403 && providerError.code !== 'CredentialsError' && providerError.code !== 'ExpiredTokenException') || providerError.statusCode === 429) ) { const nextTryNum = numTry + 1; const jitter = Math.random() * 3000 - 1000; // backoff is between 4 and 7 seconds const backOff = BASE_BACKOFF + jitter; log.info( [ `Recoverable error occurred (${e.message}), sleeping for ~${Math.round( backOff / 1000 )} seconds.`, `Try ${nextTryNum} of ${MAX_RETRIES}`, ].join(' ') ); await wait(backOff); return persistentRequest(f, nextTryNum); } throw e; } }; const request = await requestQueue.add(() => persistentRequest(async () => { const requestId = ++requestCounter; const awsService = getServiceInstance(service, method); awsLog.debug('[%d] %O %s %O', requestId, service, method, args); const req = awsService[method](...args); try { const result = await req.promise(); awsLog.debug('[%d] %O', requestId, result); return result; } catch (err) { awsLog.debug('[%d] %O', requestId, err); let message = err.message != null ? err.message : String(err.code); if (message.startsWith('Missing credentials in config')) { // Credentials error // If failed at last resort (EC2 Metadata check) expose a meaningful error // with link to AWS documentation // Otherwise, it's likely that user relied on some AWS creds, which appeared not correct // therefore expose an AWS message directly let bottomError = err; while (bottomError.originalError && !bottomError.message.startsWith('EC2 Metadata')) { bottomError = bottomError.originalError; } const errorMessage = bottomError.message.startsWith('EC2 Metadata') ? [ 'AWS provider credentials not found.', ' Learn how to set up AWS provider credentials', ` in our docs here: <${chalk.green('http://slss.io/aws-creds-setup')}>.`, ].join('') : bottomError.message; message = errorMessage; // We do not want to trigger the retry mechanism for credential errors throw Object.assign(new ServerlessError(errorMessage, 'AWS_CREDENTIALS_NOT_FOUND'), { providerError: Object.assign({}, err, { retryable: false }), }); } const providerErrorCodeExtension = (() => { if (!err.code) return 'ERROR'; if (typeof err.code === 'number') return `HTTP_${err.code}_ERROR`; return normalizeErrorCodePostfix(err.code); })(); if (err.stack) { log.debug(`${err.stack}\n${'-'.repeat(100)}`); } throw Object.assign( new ServerlessError( message, `AWS_${normalizeErrorCodePostfix(service.name)}_${normalizeErrorCodePostfix( method )}_${providerErrorCodeExtension}` ), { providerError: err, providerErrorCodeExtension, } ); } }) ); return request; } awsRequest.memoized = memoize(awsRequest, { promise: true, normalizer: ([service, method, args]) => { if (!isObject(service)) service = { name: ensureString(service) }; return [ JSON.stringify(deepSortObjectByKey(service)), method, JSON.stringify(deepSortObjectByKey(args)), ].join('|'); }, }); module.exports = awsRequest;