mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
458 lines
16 KiB
JavaScript
458 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
/* eslint-disable no-console */
|
|
|
|
const BbPromise = require('bluebird');
|
|
const path = require('path');
|
|
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');
|
|
const userStats = require('../../utils/userStats');
|
|
|
|
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,
|
|
stage: this.serverless.service.provider.stage,
|
|
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: key.toLowerCase(),
|
|
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 trackingData = {
|
|
tenant: deploymentData.tenant,
|
|
app: deploymentData.app,
|
|
};
|
|
userStats.track('service_published', trackingData);
|
|
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;
|