Skip to content

Configuration validation backed by schema #6562

Description

@medikoo

I've just put websockets instead of websocket and deploy was successful, altough no Websocket API and no binding was setup.

It'll be great if such issues are picked and communicated by the framework, Still it should be done with respect to plugins ,which may support events that are not supported at Framework Core.


Ideally we should have fine grain validation towards serverless.yml (backed with library as @hapi/joi), which can be extended by the plugins.

With v1 we can introduce a non-breaking version that'll just show warnings, while with next major it can throw meaningful errors.

Validation should be made after all variables are resolved.


As introducing complete validation for whole config as we support now seems non-trivial and very time consuming task. It'll be good to start with something minimal that provides basic validation and just allows additional properties in places where schema is not complete.

It'll allow us to deliver early, and then improve and fine tune schema on the go, and cover new functionalities with complete schema upfront.


Where validation logic should be attached?

Validation should not be configured as an internal plugin but be a part of a core (so be incorporated into engine which is also responsible for load of config, resolving variables, initializing plugins). It should be considered as a core feature.

When validation should happen?

Ideally validation should happen after all plugins were initialized and variables resolved but before life-cycle engine jump in.

Technically it's when current service.validate() happens:

validate() {
_.forEach(this.functions, (functionObj, functionName) => {
if (!_.isArray(functionObj.events)) {
throw new ServerlessError(
`Events for "${functionName}" must be an array, not an ${typeof functionObj.events}`
);
}
});
const provider = this.serverless.getProvider('aws');
if (provider) {
const stage = provider.getStage();
this.getAllFunctions().forEach(funcName => {
_.forEach(this.getAllEventsInFunction(funcName), event => {
if (_.has(event, 'http') && !validAPIGatewayStageNamePattern.test(stage)) {
throw new this.serverless.classes.Error(
[
`Invalid stage name ${stage}:`,
'it should contains only [-_a-zA-Z0-9] for AWS provider if http event are present',
'according to API Gateway limitation.',
].join(' ')
);
}
});
});
}
return this;
}

Which already tries to do some validation which should we replace and back with new schema validator

What objects should validation cover?

Some problem is that current config handling is not clean. We do not have one object with which we work, but we span config parts on our service instance here:

if (_.isObject(serverlessFile.service)) {
that.serviceObject = serverlessFile.service;
that.service = serverlessFile.service.name;
} else {
that.serviceObject = { name: serverlessFile.service };
that.service = serverlessFile.service;
}
that.app = serverlessFile.app;
that.tenant = serverlessFile.tenant;
that.org = serverlessFile.org;
that.custom = serverlessFile.custom;
that.plugins = serverlessFile.plugins;
that.resources = serverlessFile.resources;
that.functions = serverlessFile.functions || {};
// merge so that the default settings are still in place and
// won't be overwritten
that.provider = _.merge(that.provider, serverlessFile.provider);
if (serverlessFile.package) {
that.package.individually = serverlessFile.package.individually;
that.package.path = serverlessFile.package.path;
that.package.artifact = serverlessFile.package.artifact;
that.package.exclude = serverlessFile.package.exclude;
that.package.include = serverlessFile.package.include;
that.package.excludeDevDependencies = serverlessFile.package.excludeDevDependencies;
}
if (that.provider.name === 'aws') {
that.layers = serverlessFile.layers || {};
}
that.outputs = serverlessFile.outputs;

and then work with that. Variables resolution is made on service instance.

Ideally after Variables resolution is made. I think we should prepare a plain object that derives from service as follows:

const configToValidate = {
  service: service.serviceObject,
  app: service.app,
  org: service.org,
  custom: service.custom,
  plugins: service.plugins,
  resources: service.resources,
  functions: service.functions.
  provider: service.provider,
  package: service.package,
  layers: service.layers,
  outputs: service.outputs
}

And validate that.

What validation library should we use?

In consideration:

We need a validation tool that allows to easily in generic way extend base schema (e.g. define a new property on some object which is a deep nested property in global schema).

So we don't have to upfront define those eventual extension points, as otherwise we can easily run into maintenance issues in a future (it's likely that we will need more and more customization options on the way)

I'm not that familiar with@hapi/joi and I'm not sure if it provides easy means for that (by first look, it appears to me it doesn't, but I might have missed something)

We may also consider: https://github.com/epoberezkin/ajv which is based on JSON schema standard (so schema on it's own is a JSON, therefore extensible). It also supports $merge instructions dedicated for that https://github.com/epoberezkin/ajv#merge-and-patch-keywords

Before we jump into implementation, let's prepare a working a example, where we have some example schema with deep nested objects, and then we extend some deep objects with schema for additional properties.

Having that we could be more confident that we've chosen right schema validator.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions