mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
250 lines
8.6 KiB
JavaScript
250 lines
8.6 KiB
JavaScript
import _ from 'lodash';
|
|
import memoize from 'memoizee';
|
|
import PromiseQueue from 'promise-queue';
|
|
import sdk from './sdk-v2.js';
|
|
import ServerlessError from '../serverless-error.js';
|
|
import utils from '@serverlessinc/sf-core/src/utils.js';
|
|
import HttpsProxyAgent from 'https-proxy-agent';
|
|
import url from 'url';
|
|
import https from 'https';
|
|
import fs from 'fs';
|
|
import deepSortObjectByKey from '../utils/deep-sort-object-by-key.js';
|
|
import ensureString from 'type/string/ensure.js';
|
|
import isObject from 'type/object/is.js';
|
|
import wait from 'timers-ext/promise/sleep.js';
|
|
|
|
const { log } = utils;
|
|
|
|
// Activate AWS SDK logging
|
|
const awsLog = log.get('sls:aws:request');
|
|
|
|
// 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(`request: #${requestId} ${service.name}.${method}`, args);
|
|
const req = awsService[method](...args);
|
|
try {
|
|
const result = await req.promise();
|
|
awsLog.debug(`request result: #${requestId} ${service.name}.${method}`, result);
|
|
return result;
|
|
} catch (err) {
|
|
awsLog.debug(`request error: #${requestId} - ${service.name}.${method}`, 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: 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('|');
|
|
},
|
|
});
|
|
|
|
export default awsRequest;
|