UNPKG

@connected-home/serverless

Version:

The Serverless Application Framework - Powered By Amazon Web Services - http://www.serverless.com

560 lines (456 loc) 19.2 kB
'use strict'; const SError = require('./Error'), BbPromise = require('bluebird'), httpsProxyAgent = require('https-proxy-agent'), path = require('path'), _ = require('lodash'), url = require('url'), fs = require('fs'), fse = require('fs-extra'), os = require('os'), guid = require('./utils/guid'); // Load AWS Globally for the first time const AWS = require('aws-sdk'); module.exports = function(S) { function persistentRequest(f) { return new BbPromise(function(resolve, reject){ let doCall = function(){ f() .then(resolve) .catch(function(error) { if( error.statusCode == 429 ) { S.utils.sDebug("'Too many requests' received, sleeping 5 seconds"); setTimeout( doCall, 5000 ); } else reject( error ); }); }; return doCall(); }); }; class ServerlessProviderAws { constructor(config) { // Defaults this._config = config || {}; this.sdk = AWS; // We recommend you use the "request" method instead // Use HTTPS Proxy (Optional) let proxy = process.env.proxy || process.env.HTTP_PROXY || process.env.http_proxy || process.env.HTTPS_PROXY || process.env.https_proxy; if (proxy) { let proxyOptions; proxyOptions = url.parse(proxy); proxyOptions.secureEndpoint = true; AWS.config.httpOptions.agent = new httpsProxyAgent(proxyOptions); } // Configure the AWS Client timeout (Optional). The default is 120000 (2 minutes) let timeout = process.env.AWS_CLIENT_TIMEOUT || process.env.aws_client_timeout; if (timeout) { AWS.config.httpOptions.timeout = parseInt(timeout, 10); } // Configure the AWS Retry Delay to mitigate AWS API calls rate limits let retryDelay = process.env.AWS_RETRY_DELAY || process.env.aws_retry_delay; if (retryDelay) { AWS.config.update({retryDelayOptions: {base: retryDelay}}); } // Detect Profile Prefix. Useful for multiple projects (e.g., myproject_prod) this._config.profilePrefix = process.env['AWS_PROFILE_PREFIX'] ? process.env['AWS_PROFILE_PREFIX'] : null; if (this._config.profilePrefix && this._config.profilePrefix.charAt(this._config.profilePrefix.length - 1) !== '_') { this._config.profilePrefix = this._config.profilePrefix + '_'; } this.validRegions = [ 'us-east-1', 'us-west-2', // Oregon 'eu-west-1', // Ireland 'eu-central-1', // Frankfurt 'ap-northeast-1' // Tokyo ]; this.apisCache = {}; } /** * Request * - Perform an SDK request */ request(service, method, params, stage, region, options) { let _this = this; return persistentRequest( ()=> _this.getCredentials(stage, region) .then(function(credentials) { let awsService = new _this.sdk[service](credentials); let req = awsService[method](params); // TODO: Add listeners, put Debug statments here... // req.on('send', function (r) {console.log(r)}); return new BbPromise(function (res, rej) { req.send(function (err, data) { if (err) { rej(err); } else { res(data); } }); }); }) ) } /** * Get Provider Name */ getProviderName() { return 'Amazon Web Services'; } /** * Add credentials, if present, from the serverless configuration * @param credentials The credentials to add configuration credentials to * @param config The serverless configuration */ addConfigurationCredentials(credentials, config) { // just transfer the credentials if (config) { if (config.awsAdminKeyId) { credentials.accessKeyId = config.awsAdminKeyId; } if (config.awsAdminSecretKey) { credentials.secretAccessKey = config.awsAdminSecretKey; } if (config.awsAdminSessionToken) { credentials.sessionToken = config.awsAdminSessionToken; } } } addRemoteCredentials(credentials) { // only run in an enviroment that supprts remote creds if( !process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] && !process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI']){ return BbPromise.resolve() } const remoteCredentials = new AWS.RemoteCredentials() return remoteCredentials.getPromise().then(()=>{ if (remoteCredentials.accessKeyId) { credentials.accessKeyId = remoteCredentials.accessKeyId; } if (remoteCredentials.secretAccessKey) { credentials.secretAccessKey = remoteCredentials.secretAccessKey; } if (remoteCredentials.sessionToken) { credentials.sessionToken = remoteCredentials.sessionToken; } }) .catch((e) => { throw new SError(`Failed to fetch remote credentials role: ${e}`, e); }); } /** * Add credentials, if present, from the environment * @param credentials The credentials to add environment credentials to * @param prefix The environment variable prefix to use in extracting credentials from the environment */ addEnvironmentCredentials(credentials, prefix) { // separate credential environment variable prefix from obtaining the credentials from the environment. let environmentCredentials = new AWS.EnvironmentCredentials(prefix); if (environmentCredentials) { if (environmentCredentials.accessKeyId) { credentials.accessKeyId = environmentCredentials.accessKeyId; } if (environmentCredentials.secretAccessKey) { credentials.secretAccessKey = environmentCredentials.secretAccessKey; } if (environmentCredentials.sessionToken) { credentials.sessionToken = environmentCredentials.sessionToken; } } } /** * Add credentials from a profile, if the profile exists * @param credentials The credentials to add profile credentials to * @param prefix The prefix to the profile environment variable */ addProfileCredentialsImpl(credentials, prefix) { // separate profile environment variable prefix from obtaining credentials from the profile. let profile = process.env[prefix + '_PROFILE']; if (profile) { return this.getProfile(profile, true) .then((profileCredentials) => { _.assign(credentials, profileCredentials || {}); }); } else { return BbPromise.resolve(null); } } /** * Add credentials from a profile, if the profile exists adding the profile name prefix if supplied * @param credentials The credentials to add profile credentials to * @param prefix The prefix to the profile environment variable */ addProfileCredentials(credentials, prefix) { if (this._config.profilePrefix) { prefix = this._config.profilePrefix + prefix; } return this.addProfileCredentialsImpl(credentials, prefix); } /** * Get Credentials * - Fetches credentials from ENV vars via profile, access keys, or session token * - Don't use AWS.EnvironmentCredentials, since we want to require "AWS" in the ENV var names, otherwise provider trampling could occur * - TODO: Remove Backward Compatibility: Older versions include "ADMIN" in env vars, we're not using that anymore. Too long. */ getCredentials(stage, region) { let credentials = {region: region}; stage = stage ? stage.toUpperCase() : null; // implicitly already in the config... this.addConfigurationCredentials(credentials, S.config); // use the given configuration credentials if they are the only available credentials. // first from environment this.addEnvironmentCredentials(credentials, 'AWS'); // allow for Amazon standard credential environment variable prefix. this.addEnvironmentCredentials(credentials, 'SERVERLESS_ADMIN_AWS'); // but override with more specific credentials if these are also provided. this.addEnvironmentCredentials(credentials, 'AWS_' + stage); // and also override these with the Amazon standard *stage specific* credential environment variable prefix. this.addEnvironmentCredentials(credentials, 'SERVERLESS_ADMIN_AWS_' + stage); // finally override all prior with Serverless prefixed *stage specific* credentials if these are also provided. return BbPromise.resolve(credentials) // next from profile .then(() => this.addProfileCredentials(credentials, 'AWS')) // allow for generic Amazon standard prefix based profile declaration .then(() => this.addProfileCredentials(credentials, 'SERVERLESS_ADMIN_AWS')) // allow for generic Serverless standard prefix based profile declaration .then(() => this.addProfileCredentials(credentials, 'AWS_' + stage)) // allow for *stage specific* Amazon standard prefix based profile declaration .then(() => this.addProfileCredentials(credentials, 'SERVERLESS_ADMIN_AWS_' + stage)) // allow for *stage specific* Serverless standard prefix based profile declaration .then(() => this.addRemoteCredentials(credentials)) // allow for remote credentials inside an AWS container .then(() => { // if they aren't loaded now, the credentials weren't provided by a valid means if (!credentials.accessKeyId || !credentials.secretAccessKey) { throw new SError("Cant find AWS credentials", SError.errorCodes.MISSING_AWS_CREDS); } return credentials; }); } /** * Save Credentials * - Saves AWS API Keys to a profile on the file system */ saveCredentials(accessKeyId, secretKey, profileName, stage) { let configDir = this.getConfigDir(); // Create ~/.aws folder if does not exist if (!S.utils.dirExistsSync(configDir)) { fse.mkdirsSync(configDir); } let profileEnvVar = (stage ? 'AWS_' + stage + '_PROFILE' : 'AWS_PROFILE').toUpperCase(); S.utils.sDebug('Setting new AWS profile:', profileName); // Write to AWS credentials file. fs.appendFileSync( this.getAwsCredentialsFile(), os.EOL + '[' + profileName + ']' + os.EOL + 'aws_access_key_id=' + accessKeyId + os.EOL + 'aws_secret_access_key=' + secretKey + os.EOL); } /** * Get the directory containing AWS configuration files */ getConfigDir() { let env = process.env; let home = env.HOME || env.USERPROFILE || (env.HOMEPATH ? ((env.HOMEDRIVE || 'C:/') + env.HOMEPATH) : null); if (!home) { throw new SError('Cant find homedir', SError.errorCodes.MISSING_HOMEDIR); } return path.join(home, '.aws'); } /** * Get the path to the AWS credentials file */ getAwsCredentialsFile() { if (process.env.AWS_SHARED_CREDENTIALS_FILE) { return process.env.AWS_SHARED_CREDENTIALS_FILE; } let configDir = this.getConfigDir(); return path.join(configDir, 'credentials'); } /** * Get the path to the AWS config file */ getAwsConfigFile() { if (process.env.AWS_CONFIG_FILE) { return process.env.AWS_CONFIG_FILE; } let configDir = this.getConfigDir(); return path.join(configDir, 'config'); } /** * Get All Profiles * - Gets all profiles from AWS credentials, config file */ getAllProfiles() { let credsPath = this.getAwsCredentialsFile(); let configPath = this.getAwsConfigFile(); let creds; try { creds = AWS.util.ini.parse(AWS.util.readFileSync(credsPath)); } catch (e) { creds = {}; } let configs; try { configs = AWS.util.ini.parse(AWS.util.readFileSync(configPath)); } catch (e) { configs = {}; } // First, load up all profile from config file. var profiles = Object.keys(configs).reduce((obj, key) => { const match = key.match(/^profile (.+)/); if (match) { obj[match[1]] = configs[key]; } return obj; }, {}); // Now, load profiles from credentials file (overriding any values found in config file) Object.keys(creds).forEach((name) => { profiles[name] = Object.assign(profiles[name] || {}, creds[name]); }); return profiles; } /** * Get Profile * - Gets a single profile from AWS credentials, config files. */ getProfile(awsProfile, optional) { let profiles = this.getAllProfiles(); let profileConfig = profiles[awsProfile]; if (!profileConfig) { if (optional) { return BbPromise.resolve(null); } else { throw new SError(`Cant find profile ${awsProfile} in AWS credential file and/or AWS config file`, awsProfile); } } var isRoleProfile = ( profileConfig.source_profile && profileConfig.role_arn ); var getCredentials = isRoleProfile ? this.getRoleCredentials(awsProfile, profiles) : BbPromise.resolve(profileConfig); return getCredentials .then(this.canonicalizeProfileCredentials); } /** * Translate an object that holds credentials in "profile format" - i.e. * populated by profile entries in AWS config files - into * the credentials format used by the AWS SDK. Any credentials already in * the SDK format are preserved. */ canonicalizeProfileCredentials(credentials) { let result = {}; result.accessKeyId = credentials.accessKeyId || credentials.aws_access_key_id; result.secretAccessKey = credentials.secretAccessKey || credentials.aws_secret_access_key; result.sessionToken = credentials.sessionToken || credentials.aws_session_token || credentials.aws_security_token; // python boto standard return _.omitBy(result, _.isNil); } /** * Get Role Credentials * - Gets temporary credentials via assuming a role */ getRoleCredentials(profile, profiles) { const sourceProfile = profiles[profile].source_profile; const roleArn = profiles[profile].role_arn; let sourceProfileCredentials = profiles[sourceProfile]; if (!sourceProfileCredentials) { throw new SError(`Cant find source profile ${sourceProfile} in AWS credential file and/or AWS config file`, sourceProfile); } sourceProfileCredentials = this.canonicalizeProfileCredentials(sourceProfileCredentials); const sourceAccessKeyId = sourceProfileCredentials.accessKeyId; const sourceSecretAccessKey = sourceProfileCredentials.secretAccessKey; const sourceSessionToken = sourceProfileCredentials.sessionToken; if (!(sourceAccessKeyId && sourceSecretAccessKey)) { throw new SError(`Cant find credentials for source profile ${sourceProfile} in AWS credential file and/or AWS config file`, sourceProfile); } const stsCredentials = { accessKeyId: sourceAccessKeyId, secretAccessKey: sourceSecretAccessKey, }; if (sourceSessionToken) { stsCredentials[sessionToken] = sourceSessionToken; } const stsConfig = _.assign(AWS.config, stsCredentials); const STS = BbPromise.promisifyAll(new AWS.STS(stsConfig)); const assumeRoleParams = { RoleArn: roleArn, RoleSessionName: profile+"-"+guid(), }; return STS.assumeRoleAsync(assumeRoleParams) .then((res) => { return { aws_access_key_id: res.Credentials.AccessKeyId, aws_secret_access_key: res.Credentials.SecretAccessKey, aws_session_token: res.Credentials.SessionToken }; }) .catch((e) => { throw new SError(`Failed to assume role ${roleArn}: ${e}`, roleArn, e); }); } getLambdasStackName(stage, projectName) { return [projectName, stage, 'l'].join('-'); } getResourcesStackName(stage, projectName) { return [projectName, stage, 'r'].join('-'); } /** * Get REST API By Name */ getApiByName(apiName, stage, region) { let _this = this; // Validate Length if (apiName.length > 1023) { throw new SError('"' + apiName + '" cannot be used as a REST API name because it\'s over 1023 characters. Please make it shorter.'); } // Sanitize apiName = apiName.trim(); if (this.apisCache[ apiName ] && this.apisCache[ apiName ][ region ] && this.apisCache[ apiName ][ region ][ stage ]) { S.utils.sDebug( "" + stage + " - " + region + ": Found cached REST API on AWS API Gateway with name: " + apiName ); return BbPromise.resolve( this.apisCache[ apiName ][ region ][ stage ] ); } let params = { limit: 500 }; // List all REST APIs return this.request('APIGateway', 'getRestApis', params, stage, region) .then(function (response) { let restApi = null, found = 0; // Find REST API w/ same name as project for (let i = 0; i < response.items.length; i++) { if (response.items[i].name === apiName) { restApi = response.items[i]; found++; S.utils.sDebug( '"' + stage + ' - ' + region + '": found existing REST API on AWS API Gateway with name: ' + apiName); } } // Throw error if they have multiple REST APIs with the same name if (found > 1) { throw new SError('You have multiple API Gateway REST APIs in the region ' + region + ' with this name: ' + apiName); } if (restApi) { if (!_this.apisCache[ apiName ]) _this.apisCache[ apiName ] = {}; if (!_this.apisCache[ apiName ][ region ]) _this.apisCache[ apiName ][ region ] = {}; if (!_this.apisCache[ apiName ][ region ][ stage ]) _this.apisCache[ apiName ][ region ][ stage ] = restApi; return restApi; } }); } getAccountId(stage, region) { let vars = S.getProject() .getRegion(stage, region) .getVariables(); if(vars.accountId) { return vars.accountId; } else { return vars.iamRoleArnLambda .replace('arn:aws:iam::', '') .split(':')[0]; } } } return ServerlessProviderAws; };