Config schema validation by preshetin · Pull Request #7335 · serverless/serverless · GitHub
Skip to content

Config schema validation#7335

Merged
medikoo merged 130 commits into
serverless:masterfrom
preshetin:config-schema-validation
Aug 3, 2020
Merged

Config schema validation#7335
medikoo merged 130 commits into
serverless:masterfrom
preshetin:config-schema-validation

Conversation

@preshetin

@preshetin preshetin commented Feb 13, 2020

Copy link
Copy Markdown
Contributor

What did you implement

This PR implements the validation of the essential parts of serverless.yml config backed by AJV.

This validation architecture consists of one schema that is dynamically extended by plugins (both internal and external). After the schema is extended by plugins, it is used to perform the validation of the user's config.

This PR implements validation engine with relaxed schema. Fine-tuning schema parts is expected to be covered by other PR's.

Closes #6562

How can we verify it

This serverless.yml file contains errors which this PR should log to the console.

# try to use invalid schema name: '1first-digit-is-not-allowed'
service: some-service

# plugins:
#   - newEventPlugin

provider:
  name: aws
  stage: dev

functions:
  someFunc:
    handler: handler.main
    events:
      # 'httpApii' method is invalid so it logs an error
      - httpApii: get foo

Todos

Useful Scripts
  • npm run test:ci --> Run all validation checks on proposed changes
  • npm run lint:updated --> Lint all the updated files
  • npm run lint:fix --> Automatically fix lint problems (if possible)
  • npm run prettier-check:updated --> Check if updated files adhere to Prettier config
  • npm run prettify:updated --> Prettify all the updated files

Is this ready for review?: YES
Is it a breaking change?: NO

@preshetin preshetin requested a review from medikoo February 13, 2020 15:03
@preshetin

Copy link
Copy Markdown
Contributor Author

@medikoo medikoo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@preshetin Thanks for taking a start on that.

As it's a sensitive and significant feature, I think before jumping into an actual implementation, it'll be good to agree on some basic implementation spec.

Let me outline, what I think of it:

Where validation logic should be attached?

I believe that validation should not be configured as a plugin but be a part of a core (so be incorporated into engine which is also responsible for load of config, resolving variables, intiializing 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 back with schema.

I would replace this logic with new schema validation, and ensure upfront we cover the validation it's covered now.

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.


@pmuens what are your thoughts on that?

@medikoo

medikoo commented Feb 14, 2020

Copy link
Copy Markdown
Contributor

The logs indicate that the latest version of @hapi/joi uses the object spread operator which is not supported by Node 6.

For now just follow with Node v6. Hopefully we'd be able to drop support for it shortly

@preshetin

preshetin commented Feb 17, 2020

Copy link
Copy Markdown
Contributor Author

@medikoo thank you for this guidance. Next time I'll try to start by gathering feedback first before submitting a PR.

I agree with all your points. It would require slight adjustments to this PR (like moving validation logic from package plugin to core Service class) which I am ready to implement.

Also, do you agree with the proposed multi-schema approach? Using many small schemas would allow plugins to easily extend any piece of the schema. Otherwise, I don't think that extending a branch in a big tree-like schema is possible in @hapi/joi lib.

Would you like me to implement changes, or it is better to wait for @pmuens feedback?

Also, I assume we keep sticking with Joi lib, although it does not support Node v6. Hopefully, Node v6 support will be dropped by the time this PR is merged.

@medikoo

medikoo commented Feb 18, 2020

Copy link
Copy Markdown
Contributor

I assume we keep sticking with Joi lib, although it does not support Node v6. Hopefully, Node v6 support will be dropped by the time this PR is merged.

I think we can safely assume it's a v2 feature (in that case we may even crash on errors). We plan to rollout v2 in less than a month I think. So it shouldn't be blocked for too long.

Also, do you agree with the proposed multi-schema approach?

I think 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've outlined my concerns at the bottom of description here #6562

Let's continue discussion there (I've also pinned that issue, which I cannot do with PR, so hopefully we get additional feedback from community). After we agree over there on implementation aspects, let's just continue work on implementation over here

@pmuens pmuens left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea! I was always looking for a formal spec for serverless.yml. Ideally we'd put version: 1 or something like that in serverless.yml so that we can have multiple validation schemas (and therefore version serverless.yml).

Only question I have is whether this breaks custom plugins. The main problem is that custom plugins can extend the serverless object in whatever way they want. The "official" documentation says that only the custom property should be used by plugin authors but AFAIK this is not widely known.

@medikoo

medikoo commented Jul 31, 2020

Copy link
Copy Markdown
Contributor

@preshetin I've updated the implementation with new version of normalizeAjvErrors.js, I hope you're not upset I've rewritten it from scratch, it was easier for me to approach it without any baggage, especially that our points of view were slightly different.

If you have opportunity please take a look into it, I've tried my best in documenting logic. I'd like to be sure I didn't remove any valuable functionality which was covered in your version.

I've also changed slightly approach in unspecified provider validation - in all (but "off") cases it's just dedicated warning reported.

I've also already created github issues to cover schemas for all config parts which we left not specified fully, and provided links to corresponding issues in code.

I've tested it, and it seems to work really well, would love to publish it on monday! :)

@preshetin

Copy link
Copy Markdown
Contributor Author

@medikoo I'm not upset at all. In your place, I'd do the same as it is often easier to redo from scratch than understanding someone's logic.

I tested it and it looks pretty good. The new way of showing errors under one list feels more uniform.

One small regression I've noticed. In case of enum error type, the list of allowed values is not shown. I remember you wanted to have it from the very start. To verify it, try setting configValidationMode to warnn. The array of allowed values can be found inside the initial AJV error at params.allowedValues.

When I have time for open source, I'd be happy to jump into one of those numerous issues on fine-tuning other schema parts.

I don't want to be too cheerful about the release because many times before it was postponed. In case this happens, I hope it will go smoothly.

@medikoo

medikoo commented Aug 3, 2020

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configuration validation backed by schema

5 participants