'use strict'; /* eslint-disable no-console */ const BbPromise = require('bluebird'); const path = require('path'); const uuid = require('uuid'); const _ = require('lodash'); const fs = require('fs'); const fsExtra = require('../../utils/fs/fse'); const crypto = require('crypto'); const platform = require('@serverless/platform-sdk'); const getAccessKey = require('../../utils/getAccessKey'); const isLoggedIn = require('../../utils/isLoggedIn'); class Platform { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('aws'); // NOTE for the time being we only track services published to AWS if (this.provider) { this.hooks = { 'after:deploy:finalize': this.publishService.bind(this), 'after:remove:remove': this.archiveService.bind(this), }; } } getReadme() { const readmePath = path.join(this.serverless.config.servicePath, 'README.md'); if (fs.existsSync(readmePath)) { return fsExtra.readFileSync(readmePath).toString('utf8'); } return null; } getS3Type(s3Event) { const splittedS3Event = s3Event.split(':'); if (splittedS3Event[1] === 'ReducedRedundancyLostObject') { return 'aws.s3.ReducedRedundancyLostObject'; } else if (splittedS3Event[1] === 'ObjectCreated' || splittedS3Event[1] === 'ObjectRemoved') { if (splittedS3Event[2] === '*') { return `aws.s3.${splittedS3Event[1]}`; } else if (typeof splittedS3Event[2] === 'string') { return `aws.s3.${splittedS3Event[1]}.${splittedS3Event[2]}`; } } return `aws.s3.${splittedS3Event[1]}`; } getFunctionData(fn) { const fnData = { functionId: this.serverless.service.getFunction(fn).name, details: { runtime: this.serverless.service.functions[fn].runtime, memory: this.serverless.service.functions[fn].memory, timeout: this.serverless.service.functions[fn].timeout, }, package: { handler: this.serverless.service.functions[fn].handler, name: this.serverless.service.functions[fn].name, arn: `arn:aws:lambda:${this.serverless.service.provider.region}:${ this.data.service.provider.accountId}:function:${ this.serverless.service.getFunction(fn).name}`, }, }; return fnData; } getServiceData() { const serviceData = { name: this.serverless.service.service, stage: this.serverless.processedInput.options.stage || this.serverless.service.provider.stage, provider: { name: this.serverless.service.provider.name, region: this.serverless.service.provider.region, accountId: this.serverless.service.provider.accountId, }, pluginsData: this.serverless.service.pluginsData, readme: this.getReadme(), }; if (this.serverless.service.serviceObject.description) { serviceData.description = this.serverless.service.serviceObject.description; } if (this.serverless.service.serviceObject.license) { serviceData.license = this.serverless.service.serviceObject.license; } if (this.serverless.service.serviceObject.bugs) { serviceData.bugs = this.serverless.service.serviceObject.bugs; } if (this.serverless.service.serviceObject.repository) { serviceData.repository = this.serverless.service.serviceObject.repository; } if (this.serverless.service.serviceObject.homepage) { serviceData.homepage = this.serverless.service.serviceObject.homepage; } return serviceData; } getScheduledSubscription(event, fn) { const provider = this.data.service.provider; const subscription = { functionId: this.serverless.service.getFunction(fn).name, type: 'aws.cloudwatch.scheduled', details: { function: this.serverless.service.getFunction(fn).name, source: 'AWS::CloudWatch::Scheduled', }, provider, permissions: { type: 'aws IAM', action: 'lambda:InvokeFunction', sourceAccount: 'Amazon', }, }; if (typeof event === 'string') { subscription.details.rate = event; subscription.details.name = null; subscription.details.description = null; } else if (typeof event === 'object') { subscription.details.rate = event.rate; subscription.details.name = event.name; subscription.details.description = event.description; } subscription.event = event; return subscription; } getS3Subscription(event, fn) { const provider = this.data.service.provider; const subscription = { functionId: this.serverless.service.getFunction(fn).name, details: { function: this.serverless.service.getFunction(fn).name, }, provider, permissions: { type: 'aws IAM', action: 'lambda:InvokeFunction', sourceAccount: 'Amazon', }, }; if (typeof event === 'string') { subscription.type = 'aws.s3.ObjectCreated'; subscription.details.source = `AWS::S3::${event}`; subscription.details.bucket = event; subscription.details.event = 's3:ObjectCreated:*'; subscription.details.rules = null; subscription.permissions.sourceArn = `arn:aws:s3:::${event}`; } else if (typeof event === 'object') { subscription.type = this.getS3Type(event.event); subscription.details.source = `AWS::S3::${event.bucket}`; subscription.details.bucket = event.bucket; subscription.details.event = event.event; subscription.details.rules = event.rules || null; subscription.permissions.sourceArn = `arn:aws:s3:::${event.bucket}`; } subscription.event = event; return subscription; } getSnsSubscription(event, fn) { // todo existing topic arn const provider = this.data.service.provider; const subscription = { functionId: this.serverless.service.getFunction(fn).name, type: 'aws.sns', details: { function: this.serverless.service.getFunction(fn).name, }, provider, permissions: { type: 'aws IAM', action: 'lambda:InvokeFunction', sourceAccount: 'Amazon', }, }; if (typeof event === 'string') { subscription.details.source = `AWS::SNS::${event}`; subscription.details.topic = event; subscription.permissions.sourceArn = `arn:aws:sns:${this.provider.getRegion() }:${provider.accountId}:${event}`; } else if (typeof event === 'object') { subscription.details.source = `AWS::SNS::${event.topicName}`; subscription.details.topic = event.topicName; subscription.details.displayName = event.displayName; subscription.permissions.sourceArn = `arn:aws:sns:${this.provider.getRegion() }:${provider.accountId}:${event.topicName}`; } subscription.event = event; return subscription; } getEGSubscription(event, fn) { const subscription = { functionId: this.serverless.service.getFunction(fn).name, type: event.eventType, details: { function: this.serverless.service.getFunction(fn).name, type: event.type, app: this.data.app, service: this.data.service.name, stage: this.serverless.service.provider.stage, path: event.path || '/', }, provider: { name: 'Serverless', tenant: this.data.tenant, }, properties: { name: event.eventType, service: this.data.service.name, stage: this.serverless.service.provider.stage, }, }; if (event.type === 'sync') { subscription.details.source = 'Sls::EventGateway::http'; } else if (event.type === 'async') { subscription.details.source = 'sls/eventgateway/com'; } if (event.eventType === 'http.request') { subscription.details.method = event.method; } if (typeof event.cors === 'boolean' || typeof event.cors === 'object') { subscription.details.cors = event.cors; } else { subscription.details.cors = true; } subscription.event = event; return subscription; } getApigSubscription(event, fn) { const apiId = this.serverless.service.deployment.apiId; const provider = this.data.service.provider; const subscription = { functionId: this.serverless.service.getFunction(fn).name, type: 'aws.apigateway.http', details: { function: this.serverless.service.getFunction(fn).name, type: 'http', source: 'AWS::APIGateway::http', apiId, }, provider, permissions: { type: 'aws IAM', action: 'lambda:InvokeFunction', sourceAccount: 'Amazon', }, }; if (typeof event === 'string') { subscription.details.method = event.split(' ')[0]; subscription.details.path = event.split(' ')[1]; subscription.permissions.sourceArn = this.provider .getMethodArn(provider.accountId, apiId, event.split(' ')[0], event.split(' ')[1]); subscription.details.cors = false; subscription.details.lambdaProxy = true; } else if (typeof event === 'object') { subscription.details.method = event.method; subscription.details.path = event.path; subscription.permissions.sourceArn = this.provider .getMethodArn(provider.accountId, apiId, event.method, event.path); if (typeof event.cors === 'boolean' || typeof event.cors === 'object') { subscription.details.cors = true; } else { subscription.details.cors = false; } if (event.integration === 'AWS') { subscription.details.lambdaProxy = false; } else { subscription.details.lambdaProxy = true; } } subscription.event = event; // the aws package plugin strips trailing and leading slashes per CF needs. if (subscription.details.path === '') subscription.details.path = '/'; if (subscription.event.path === '') subscription.event.path = '/'; // in case of AWS integration, the aws package plugin adds // request/response objects that we need to stringify if (!subscription.details.lambdaProxy) { subscription.event.request = JSON.stringify(subscription.event.request); subscription.event.response = JSON.stringify(subscription.event.response); } return subscription; } getResources() { const frameworkResources = this.serverless.service.provider .compiledCloudFormationTemplate.Resources; const resources = []; _.forEach(frameworkResources, (value, key) => { const physicalId = _.find(this.cfResources, r => r.LogicalResourceId === key) .PhysicalResourceId; const resource = { resourceId: uuid.v4(), id: physicalId, name: key, type: value.Type, properties: JSON.stringify(value.Properties), provider: this.serverless.service.provider.name, }; resources.push(resource); }); return resources; } publishService() { if (!this.serverless.service.deployment || !this.serverless.service.deployment.deploymentId) { return BbPromise.resolve(); } this.serverless.cli.log('Publishing service to Serverless Platform...'); return this.provider.getStackResources().then(resources => { this.cfResources = resources; }).then(() => this.provider.getAccountId()) .then(accountId => { const service = this.serverless.service; this.data = { app: this.serverless.service.app, tenant: this.serverless.service.tenant, accessKey: this.serverless.service.deployment.accessKey, version: '0.1.0', service: this.getServiceData(), functions: [], subscriptions: [], resources: this.getResources(), }; this.data.service.provider.accountId = accountId; Object.keys(service.functions).forEach(fn => { const fnData = this.getFunctionData(fn); this.data.functions.push(fnData); this.serverless.service.getAllEventsInFunction(fn).forEach(event => { let subscription = { functionId: this.serverless.service.getFunction(fn).name, details: {}, provider: this.data.service.provider, permissions: {}, }; if (Object.keys(event)[0] === 'eventgateway') { subscription = this.getEGSubscription(event.eventgateway, fn); } else if (Object.keys(event)[0] === 'http') { subscription = this.getApigSubscription(event.http, fn); } else if (Object.keys(event)[0] === 'stream') { if (typeof event.stream === 'string') { const streamType = event.stream.split(':')[2]; subscription.type = `aws.${streamType}`; } else if (typeof event.stream === 'object') { if (event.stream.type === 'dynamodb') { subscription.type = 'aws.dynamodb'; } else if (event.stream.type === 'kinesis') { subscription.type = 'aws.kinesis'; } } subscription.event = event.stream; } else if (Object.keys(event)[0] === 's3') { subscription = this.getS3Subscription(event.s3, fn); } else if (Object.keys(event)[0] === 'schedule') { subscription = this.getScheduledSubscription(event.schedule, fn); } else if (Object.keys(event)[0] === 'sns') { subscription = this.getSnsSubscription(event.sns, fn); } else if (Object.keys(event)[0] === 'alexaSkill') { subscription.type = 'aws.alexa.skill'; subscription.event = event.alexaSkill; } else if (Object.keys(event)[0] === 'iot') { subscription.type = 'aws.iot'; subscription.event = event.iot; } else if (Object.keys(event)[0] === 'cloudwatchEvent') { subscription.type = 'aws.cloudwatch'; subscription.event = event.cloudwatchEvent; } else if (Object.keys(event)[0] === 'cloudwatchLog') { subscription.type = 'aws.cloudwatch.log'; subscription.event = event.cloudwatchLog; } else if (Object.keys(event)[0] === 'cognitoUserPool') { subscription.type = 'aws.cognito'; subscription.event = event.cognitoUserPool; } else if (Object.keys(event)[0] === 'alexaSmartHome') { subscription.type = 'aws.alexa.home'; subscription.event = event.alexaSmartHome; } subscription.subscriptionId = crypto.createHash('md5') .update(JSON.stringify(subscription)).digest('hex'); // dashboard currently does not support sqs if (Object.keys(event)[0] !== 'sqs') { this.data.subscriptions.push(subscription); } }); }); const deploymentData = this.serverless.service.deployment; deploymentData.status = 'Success'; deploymentData.state = this.data; return platform.updateDeployment(deploymentData) .then(() => { const serviceUrlData = { tenant: deploymentData.tenant, app: deploymentData.app, name: deploymentData.state.service.name, }; const serviceUrl = platform.getServiceUrl(serviceUrlData); this.serverless.cli .log('Successfully published your service on the Serverless Platform'); this.serverless.cli.log(`Service URL: ${serviceUrl}`); }); }); } archiveService() { if (!isLoggedIn()) { return BbPromise.resolve(); } return getAccessKey(this.serverless.service.tenant).then(accessKey => { if (!accessKey || !this.serverless.service.app || !this.serverless.service.tenant) { return BbPromise.resolve(); } const data = { name: this.serverless.service.service, tenant: this.serverless.service.tenant, app: this.serverless.service.app, accessKey, }; return platform.archiveService(data) .then(() => { this.serverless.cli.log('Successfully archived your service on the Serverless Platform'); }) .catch(err => { this.serverless.cli.log('Failed to archived your service on the Serverless Platform'); throw new this.serverless.classes.Error(err.message); }); }); } } module.exports = Platform;