Merge pull request #1453 from serverless/better-errors

more validation and better error messages
This commit is contained in:
Eslam λ Hefnawy 2016-07-04 18:35:17 +09:00 committed by GitHub
commit c7335c3ee4
16 changed files with 211 additions and 44 deletions

View File

@ -21,9 +21,26 @@ module.exports.logError = (e) => {
line = `${line}-`;
}
consoleLog(' ');
consoleLog(chalk.yellow(` ${errorType} ${line}`));
consoleLog(' ');
consoleLog(chalk.yellow(` ${e.message}`));
// format error message to fit neatly
const words = e.message.split(' ');
let logLine = [];
words.forEach(word => {
logLine.push(word);
const logLineString = logLine.join(' ');
if (logLineString.length > 50) {
consoleLog(chalk.yellow(` ${logLineString}`));
logLine = [];
}
});
if (logLine.length !== 0) {
consoleLog(chalk.yellow(` ${logLine.join(' ')}`));
}
consoleLog(' ');
if (e.name !== 'ServerlessError') {

View File

@ -38,7 +38,11 @@ class PluginManager {
validateCommands(commandsArray) {
// TODO: implement an option to get deeper than one level
if (!this.commands[commandsArray[0]]) {
throw new this.serverless.classes.Error(`command "${commandsArray[0]}" not found.`);
const errorMessage = [
`command "${commandsArray[0]}" not found`,
' Run "serverless help" for a list of all available commands.',
].join();
throw new this.serverless.classes.Error(errorMessage);
}
}

View File

@ -54,6 +54,15 @@ class Service {
throw new SError('"functions" property is missing in serverless.yaml');
}
if (['aws', 'azure', 'google', 'ibm'].indexOf(serverlessYaml.provider)) {
const errorMessage = [
`Provider "${serverlessYaml.provider}" is not supported.`,
' Valid values for provider are: aws, azure, google, ibm.',
' Please provide one of those values to the "provider" property in serverless.yaml.',
].join('');
throw new SError(errorMessage);
}
that.service = serverlessYaml.service;
that.provider = serverlessYaml.provider;
that.variableSyntax = serverlessYaml.variableSyntax;
@ -167,12 +176,22 @@ class Service {
// Populate
if (!value && !value !== '') {
const errorMessage = [
`Variable "${variableName}" doesn't exist in serverless.env.yaml.`,
' Please add it to serverless.env.yaml.',
].join('');
throw new that.serverless.classes
.Error(`Variable "${variableName}" doesn't exist in serverless.env.yaml.`);
.Error(errorMessage);
} else if (typeof value === 'string') {
if (variableString.split('.').length > 1) {
const errorMessage = [
`Trying to access sub properties of a string variable "${variableName}".`,
' Please make sure the variable in serverless.env.yaml',
' is an object, otherwise you cannot use the',
' dot notation for that variable in serverless.yaml',
].join('');
throw new that.serverless.classes
.Error('Trying to access sub properties of a string variable');
.Error(errorMessage);
}
// for string variables, we use replaceall in case the user
// includes the variable as a substring (ie. "hello ${name}")
@ -185,8 +204,14 @@ class Service {
subProperties.splice(0, 1);
subProperties.forEach(subProperty => {
if (!value[subProperty]) {
const errorMessage = [
`Variable "${variableName}" doesn't have sub property "${subProperty}".`,
' Please make sure the variable is',
' the intended object in serverless.env.yaml,',
' or reference the correct sub property in serverless.yaml',
].join('');
throw new that.serverless.classes
.Error(`Invalid sub property for variable "${variableName}"`);
.Error(errorMessage);
}
value = value[subProperty];
});
@ -195,18 +220,35 @@ class Service {
val = replaceall(variableSyntax, value, val);
} else {
if (val !== variableSyntax) {
const errorMessage = [
'Trying to populate non string variables into',
` a string for variable "${variableName}".`,
' Please make sure the variable value in',
' serverless.env.yaml is a string',
].join('');
throw new that.serverless.classes
.Error('Trying to populate non string variables into a string');
.Error(errorMessage);
}
val = value;
}
} else if (variableString.split('.').length > 1) {
const errorMessage = [
`Trying to access sub properties of a non-object variable "${variableName}"`,
' Please make sure the variable is an object in serverless.env.yaml,',
' otherwise, you cannot use the dot notation',
' for that variable in serverless.yaml',
].join('');
throw new that.serverless.classes
.Error('Trying to access sub properties of a non-object variable');
.Error(errorMessage);
} else {
if (val !== variableSyntax) {
const errorMessage = [
'Trying to populate non string variables',
` into a string for variable "${variableName}".`,
' Please make sure the variable value in serverless.env.yaml is a string',
].join('');
throw new that.serverless.classes
.Error('Trying to populate non string variables into a string');
.Error(errorMessage);
}
val = value; // not string nor object
}
@ -240,14 +282,14 @@ class Service {
if (functionName in this.functions) {
return this.functions[functionName];
}
throw new SError(`function ${functionName} doesn't exist in this Service`);
throw new SError(`Function "${functionName}" doesn't exist in this Service`);
}
getEventInFunction(eventName, functionName) {
if (eventName in this.getFunction(functionName).events) {
return this.getFunction(functionName).events[eventName];
}
throw new SError(`event ${eventName} doesn't exist in function ${functionName}`);
throw new SError(`Event "${eventName}" doesn't exist in function "${functionName}"`);
}
getAllEventsInFunction(functionName) {
@ -258,7 +300,7 @@ class Service {
if (stageName in this.environment.stages) {
return this.environment.stages[stageName];
}
throw new SError(`stage ${stageName} doesn't exist in this Service`);
throw new SError(`Stage "${stageName}" doesn't exist in this service.`);
}
getAllStages() {
@ -269,7 +311,7 @@ class Service {
if (regionName in this.getStage(stageName).regions) {
return this.getStage(stageName).regions[regionName];
}
throw new SError(`region ${regionName} doesn't exist in stage ${stageName}`);
throw new SError(`Region "${regionName}" doesn't exist in stage "${stageName}"`);
}
getAllRegionsInStage(stageName) {

View File

@ -18,8 +18,14 @@ module.exports = {
method = event.http.split(' ')[0];
path = event.http.split(' ')[1];
} else {
const errorMessage = [
`HTTP event of function ${functionName} is not an object nor a string.`,
' The correct syntax is: http: get users/list',
' OR an object with "path" and "method" proeprties.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(`HTTP event of function ${functionName} is not an object nor a string`);
.Error(errorMessage);
}
const resourceLogicalId = this.resourceLogicalIds[path];

View File

@ -18,8 +18,14 @@ module.exports = {
method = event.http.split(' ')[0];
path = event.http.split(' ')[1];
} else {
const errorMessage = [
`HTTP event of function ${functionName} is not an object nor a string.`,
' The correct syntax is: http: get users/list',
' OR an object with "path" and "method" proeprties.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(`HTTP event of function ${functionName} is not an object nor a string`);
.Error(errorMessage);
}
const normalizedMethod = method[0].toUpperCase() +

View File

@ -18,8 +18,14 @@ module.exports = {
} else if (typeof event.http === 'string') {
path = event.http.split(' ')[1];
} else {
const errorMessage = [
`HTTP event of function ${functionName} is not an object nor a string.`,
' The correct syntax is: http: get users/list',
' OR an object with "path" and "method" proeprties.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(`HTTP event of function ${functionName} is not an object nor a string`);
.Error(errorMessage);
}
while (path !== '') {

View File

@ -22,7 +22,7 @@ module.exports = {
this.serverless.service.getRegionInStage(this.options.stage, this.options.region);
// validate that path and method exists for each http event in service
forEach(this.serverless.service.functions, (functionObject) => {
forEach(this.serverless.service.functions, (functionObject, functionName) => {
functionObject.events.forEach(event => {
if (event.http) {
let method;
@ -36,14 +36,35 @@ module.exports = {
path = event.http.split(' ')[1];
}
// TODO validate the values of these properties as well
if (!path) {
const errorMessage = [
`Missing "path" property in function "${functionName}"`,
' for http event in serverless.yaml.',
' If you define an http event, make sure you pass a valid value for it,',
' either as string syntax, or object syntax.',
' Please check the docs for more options.',
].join('');
throw new this.serverless.classes
.Error('Missing "path" property in serverless.yaml for http event.');
.Error(errorMessage);
}
if (!method) {
const errorMessage = [
`Missing "method" property in function "${functionName}"`,
' for http event in serverless.yaml.',
' If you define an http event, make sure you pass a valid value for it,',
' either as string syntax, or object syntax.',
' Please check the docs for more options.',
].join('');
throw new this.serverless.classes
.Error('Missing "method" property in serverless.yaml for http event.');
.Error(errorMessage);
}
if (['get', 'post', 'put', 'patch', 'options', 'head', 'delete'].indexOf(method) === -1) {
const errorMessage = [
`Invalid APIG method "${method}" in function "${functionName}".`,
' AWS supported methods are: get, post, put, patch, options, head, delete.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
}
});

View File

@ -31,8 +31,13 @@ class AwsCompileS3Events {
if (typeof event.s3 === 'object') {
if (!event.s3.bucket) {
const errorMessage = [
`Missing "bucket" property for s3 event in function ${functionName}.`,
' The correct syntax is: s3: bucketName OR an object with "bucket" property.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(`Missing "bucket" property in s3 event in function ${functionName}`);
.Error(errorMessage);
}
BucketName = event.s3.bucket + i;
if (event.s3.event) {
@ -41,8 +46,13 @@ class AwsCompileS3Events {
} else if (typeof event.s3 === 'string') {
BucketName = event.s3 + i;
} else {
const errorMessage = [
`S3 event of function ${functionName} is not an object nor a string.`,
' The correct syntax is: s3: bucketName OR an object with "bucket" property.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(`S3 event of function ${functionName} is not an object nor a string`);
.Error(errorMessage);
}
const bucketTemplate = `
{

View File

@ -31,8 +31,14 @@ class AwsCompileScheduledEvents {
// TODO validate rate syntax
if (typeof event.schedule === 'object') {
if (!event.schedule.rate) {
const errorMessage = [
`Missing "rate" property for schedule event in function ${functionName}`,
' The correct syntax is: schedule: rate(10 minutes)',
' OR an object with "rate" property.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(`Missing "rate" property in schedule event in function ${functionName}`);
.Error(errorMessage);
}
ScheduleExpression = event.schedule.rate;
State = event.schedule.enabled ? 'ENABLED' : 'DISABLED';
@ -40,8 +46,14 @@ class AwsCompileScheduledEvents {
ScheduleExpression = event.schedule;
State = 'ENABLED';
} else {
const errorMessage = [
`Schedule event of function ${functionName} is not an object nor a string`,
' The correct syntax is: schedule: rate(10 minutes)',
' OR an object with "rate" property.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes
.Error(`Schedule event of function ${functionName} is not an object nor a string`);
.Error(errorMessage);
}
const scheduleTemplate = `

View File

@ -48,19 +48,24 @@ class AwsCompileFunctions {
.S3Key = ''; // will be replaced in a further step
if (!functionObject.handler) {
const errorMessage = [
`Missing "handler" property in function ${functionName}`,
' Please make sure you point to the correct lambda handler.',
' For example: handler.hello.',
' Please check the docs for more info',
].join('');
throw new this.serverless.classes
.Error(`Missing "handler" property in function ${functionName}`);
.Error(errorMessage);
}
// TODO validate the values of each of those properties
const Handler = functionObject.handler;
const FunctionName = functionObject.name
|| `${this.serverless.service.service}-${this.options.stage}-${functionName}`;
const MemorySize = functionObject.memory
|| this.serverless.service.defaults.memory
const MemorySize = Number(functionObject.memory)
|| Number(this.serverless.service.defaults.memory)
|| 1024;
const Timeout = functionObject.timeout
|| this.serverless.service.defaults.timeout
const Timeout = Number(functionObject.timeout)
|| Number(this.serverless.service.defaults.timeout)
|| 6;
newFunction.Properties.Handler = Handler;

View File

@ -51,7 +51,12 @@ module.exports = {
const zipFileName = `${func.name}-${(new Date).getTime().toString()}.zip`;
if (!handlerFullPath.endsWith(func.handler)) {
throw new this.serverless.classes.Error(`The handler ${func.handler} was not found`);
const errorMessage = [
`The handler ${func.handler} was not found.`,
' Please make sure you have this handler in your service at the referenced location.',
' Please check the docs for more info',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
const packageRoot = handlerFullPath.replace(func.handler, '');

View File

@ -5,7 +5,8 @@ const BbPromise = require('bluebird');
module.exports = {
validate() {
if (!this.serverless.config.servicePath) {
throw new this.serverless.classes.Error('This command can only be run inside a service');
throw new this.serverless.classes
.Error('This command can only be run inside a service directory');
}
this.options.stage = this.options.stage
@ -19,12 +20,6 @@ module.exports = {
this.serverless.service.getStage(this.options.stage);
this.serverless.service.getRegionInStage(this.options.stage, this.options.region);
if (Object.keys(this.serverless.service.environment
.stages[this.options.stage].regions[this.options.region]).indexOf('vars') === -1) {
throw new this.serverless.classes
.Error('region vars object does not exist in serverless.env.yaml');
}
return BbPromise.resolve();
},
};

View File

@ -54,10 +54,5 @@ describe('#validate()', () => {
awsDeploy.serverless.config.servicePath = false;
expect(() => awsDeploy.validate()).to.throw(Error);
});
it('should throw error if region vars object does not exist', () => {
awsDeploy.serverless.service.environment.stages.dev.regions['us-east-1'] = {};
expect(() => awsDeploy.validate()).to.throw(Error);
});
});

View File

@ -41,7 +41,7 @@ class AwsInvoke {
if (this.options.path) {
if (!this.serverless.utils
.fileExistsSync(path.join(this.serverless.config.servicePath, this.options.path))) {
throw new this.serverless.classes.Error('The file path you provided does not exist.');
throw new this.serverless.classes.Error('The file you provided does not exist.');
}
this.options.data = this.serverless.utils

View File

@ -49,7 +49,11 @@ class Create {
}
if (['aws', 'azure', 'google', 'ibm'].indexOf(this.options.provider)) {
throw new this.serverless.classes.Error('Please provide a valid provider.');
const errorMessage = [
`Provider "${this.options.provider}" is not supported.`,
' Valid values for provider are: aws, azure, google, ibm.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
this.serverless.config

View File

@ -507,6 +507,45 @@ describe('Service', () => {
});
});
it('should throw error if provider property is invalid', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date).getTime().toString());
const serverlessYaml = {
service: 'service-name',
provider: 'invalid',
functions: {},
};
const serverlessEnvYaml = {
vars: {},
stages: {
dev: {
vars: {},
regions: {},
},
},
};
serverlessEnvYaml.stages.dev.regions['us-east-1'] = {
vars: {},
};
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'),
YAML.dump(serverlessYaml));
SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.env.yaml'),
YAML.dump(serverlessEnvYaml));
const serverless = new Serverless({ servicePath: tmpDirPath });
serviceInstance = new Service(serverless);
return serviceInstance.load().then(() => {
// if we reach this, then no error was thrown as expected
// so make assertion fail intentionally to let us know something is wrong
expect(1).to.equal(2);
}).catch(e => {
expect(e.name).to.be.equal('ServerlessError');
});
});
it('should throw error if functions property is missing', () => {
const SUtils = new Utils();
const tmpDirPath = path.join(os.tmpdir(), (new Date).getTime().toString());