Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aws http event schema #8301

Merged
merged 21 commits into from Oct 8, 2020
Merged

Conversation

thewizarodofoz
Copy link
Contributor

@thewizarodofoz thewizarodofoz commented Sep 29, 2020

Addresses: #8018

This PR is getting pretty big, so I think it would be wise to limit it to the http event schema (and do the provider schema in another PR). @medikoo WDYT?

@thewizarodofoz thewizarodofoz marked this pull request as draft September 29, 2020 16:17
@codecov-commenter
Copy link

codecov-commenter commented Sep 29, 2020

Codecov Report

Merging #8301 into master will decrease coverage by 0.06%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #8301      +/-   ##
==========================================
- Coverage   88.01%   87.95%   -0.07%     
==========================================
  Files         249      249              
  Lines        9307     9255      -52     
==========================================
- Hits         8192     8140      -52     
  Misses       1115     1115              
Impacted Files Coverage Δ
...ins/aws/package/compile/events/apiGateway/index.js 100.00% <100.00%> (ø)
.../aws/package/compile/events/apiGateway/lib/cors.js 100.00% <100.00%> (ø)
.../package/compile/events/apiGateway/lib/validate.js 96.23% <100.00%> (-0.88%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d2fb696...71104ac. Read the comment docs.

@medikoo
Copy link
Contributor

medikoo commented Sep 30, 2020

This PR is getting pretty big, so I think it would be wise to limit it to the http event schema (and do the provider schema in another PR). @medikoo WDYT?

I think it's a good call, unless there are some properties which could be configured in both contexts, then I would also add them to provider section.

@thewizarodofoz
Copy link
Contributor Author

thewizarodofoz commented Oct 1, 2020

Questions:

  1. authorizer.claims - should we list the standard claims?

  2. request.parameters[location][key].required - seems to me that if the value of a parameter is an object, 'required' is required because of this logic

  3. should we share schema utils functions like arrayOrSingle, mapOfType - if so, where?

  4. not sure about all of the if/else parts, might be better to have a base schema and extend that per use-case like we did in other schemas?

@thewizarodofoz thewizarodofoz marked this pull request as ready for review October 1, 2020 15:11
@medikoo
Copy link
Contributor

medikoo commented Oct 1, 2020

@thewizarodofoz thanks for questions. I know it's a challenging one.

authorizer.claims - should we list the standard claims?

I think we may skip on that, and just accept whatever string. Trying to be strict on such detached things (as I see this is setn as into some string formartted config to AWS), may result with unwanted maintenance cost,

request.parameters[location][key].required - seems to me that if the value of a parameter is an object, 'required' is required because of this logic

I see what you mean. That if it's an object with no required property, we will assign an object where boolean is expected.
I see it as an error in our logic (which we can fix in other PR) - we can see in PR that introduced that, that there was no intention to make required required: #7897. I would definitely not imply required as required property

@medikoo
Copy link
Contributor

medikoo commented Oct 1, 2020

should we share schema utils functions like arrayOrSingle, mapOfType

After we have #8319 in, there's no need for arrayOrSingle. We should just define array type (and if singular value was passed it'll be coerced to array, by schema validation engine)

I wouldn't generalize mapOfType, there's no duplication here (as we had with arrayOrSingle), and gain is minimal (while it makes schema less apparent)

@medikoo
Copy link
Contributor

medikoo commented Oct 1, 2020

not sure about all of the if/else parts, might be better to have a base schema and extend that per use-case like we did in other schemas?

Can you elaborate a bit more? Anyway I would stay away from if/else, as it'll make reported errors obscure and not well informative. I think it's ok to implement more complex validation rules inline and let errors with meaningful messages surface

@thewizarodofoz
Copy link
Contributor Author

@medikoo ready for review

Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

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

Thank you @thewizarodofoz ! Please see my comments, I think schema for object version has one important flaw (I decided to pause my review until we clarify that)

// TODO: Complete schema, see https://github.com/serverless/serverless/issues/8018
anyOf: [{ type: 'string' }, { type: 'object' }],
oneOf: [
{ type: 'string', pattern: `^(${httpMethods.join('|')}) \\S+` },
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to use regex to be case insensitive (same as we did in HTTP API:

{ type: 'string', regexp: methodPathPattern.toString() },
)

Note that supporting just all lower case and all upper case will probably be not good enoug, as I take Get or Post also works

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think we should have the same case-insensitive treatment (using regex) for similar cases like authorizer.type and integration?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think whenever we need to use regexp pattern and expect case insensitive matching, we should use regex instead of pattern

So if it's the case for authorizer.type an integration then, yes

method: {
enum: extendWithUppercase(httpMethods),
},
path: { type: 'string' },
Copy link
Contributor

Choose a reason for hiding this comment

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

It'll be nice to use same regex as in httpApi event

Comment on lines 269 to 277
{ type: 'string', pattern: `^(${httpMethods.join('|')}) \\S+` },
baseIntegrationSchema,
awsIntegrationSchema,
cognitoIntegrationSchema,
httpIntegrationSchema,
httpVpcLinkIntegrationSchema,
lambdaIntegrationSchema,
lambdaProxyIntegrationSchema,
mockIntegrationSchema,
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we expect method and path unconditionally, while above configuration, seem to expect different set of properties exclusively (and most of them do not require method and path), that doesn't look right.

In my understanding, schema for http is either string (in form of method path), or single version of object where method and path are required.

In similar manner as in httpApi event here:

{ type: 'string', regexp: methodPathPattern.toString() },
{
type: 'object',
properties: {
authorizer: {
oneOf: [
{ type: 'string' },
{
type: 'object',
properties: {
id: {
anyOf: [{ type: 'string' }, { $ref: '#/definitions/awsCfFunction' }],
},
name: { type: 'string' },
scopes: { type: 'array', items: { type: 'string' } },
},
oneOf: [{ required: ['id'] }, { required: ['name'] }],
additionalProperties: false,
},
],
},
method: { type: 'string', regexp: methodPattern.toString() },
path: { type: 'string', regexp: /^(?:\*|\/\S*)$/.toString() },

but ofc we will have more properties in case of this event

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I declared path as required in baseIntegrationSchema, so all other integration schemas inherit that.
I did fail to declare method as required as well.
The reason I used multiple object schemas instead of one is there many conditions regarding integration types. That is what I referred to when I wrote about if/else - if I go with one object for all integration types, then I would have to use many if/else clauses to declare all constraints.

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, sorry I failed to see, that other objects, share all properties from baseIntegrationSchema

Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

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

Thank you @thewizarodofoz! It's a very challenging one, and what you've propsed looks very promising.

Please see my comments

oneOf: [
{ type: 'string' },
caseInsensitive('aws_iam'),
{ $ref: '#/definitions/awsArn' },
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should be awsArnString

oneOf: ['token', 'cognito_user_pools', 'request', 'aws_iam'].map(caseInsensitive),
},
authorizerId: {
oneOf: [{ type: 'string' }, { $ref: '#/definitions/awsCfRef' }],
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not simply awsCfInstruction? Are we sure that relying on any other, aside of Ref, intrinsic functions is not supported here?

oneOf: [{ type: 'string' }, { $ref: '#/definitions/awsCfRef' }],
},
arn: { $ref: '#/definitions/awsArn' },
resultTtlInSeconds: { type: 'integer', maximum: 3600 },
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's also set minimum

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think minimum should be 0 because:

If you specify a value greater than 0, API Gateway caches the authorizer responses

Meaning, setting 0 will disable cache. I have no experience with this, but I guess it's a valid option.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, my point was, to not welcome negative values, we should set minimum: 0 I believe

identityValidationExpression: { type: 'string' },
managedExternally: { type: 'boolean' },
name: { type: 'string' },
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's keep alphabetical order

lib/plugins/aws/package/compile/events/apiGateway/index.js Outdated Show resolved Hide resolved
properties: {
allowCredentials: { type: 'boolean' },
headers: { type: 'array', items: { type: 'string' } },
maxAge: { type: 'integer', minimum: 0 },
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should be minimum 1

Comment on lines 265 to 272
baseIntegrationSchema,
awsIntegrationSchema,
cognitoIntegrationSchema,
httpIntegrationSchema,
httpVpcLinkIntegrationSchema,
lambdaIntegrationSchema,
lambdaProxyIntegrationSchema,
mockIntegrationSchema,
Copy link
Contributor

@medikoo medikoo Oct 5, 2020

Choose a reason for hiding this comment

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

One problem with this kind of configuration is that eventual errors will not be informative.

afaik if I declare method: FOO in otherwise perfectly valid configuration, the result error will be: "Unsupported configuration format", with no hint that error is at method property.

It goes from fact, that with such schema it's challenging to resolve which exactly oneOf option was chosen by user. AJV will expose errors resolved for each option, showing all of them will produce unbearable noise (we intentionally convert it then, to one "Unsupported configuration format")

We already had similar cases before, and we decided to configure properties for all options in one level, and leave (or introduce) inline validation that's meaningful for user as e.g. follows:

if (integrationType !== X && authorizer.claims) {
  throw new ServelressError("authorizer.claims is not supported for given integration type");
}

I would also stick here to that.

I believe we should not try to validate any complex interchangeable rules with JSON schema, as it then it becomes a maintenance burden we can't show a meaningful error message to user.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand.
I will refactor this PR to have a single schema, and leave the validations needed for the edge cases.

Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

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

Thank you @thewizarodofoz, that looks very good! I've found few last potentials issues. Otherwise it seems ready to go

async: { type: 'boolean' },
authorizer: authorizerSchema,
connectionId: { type: 'string' },
connectionType: { enum: ['vpc-link', 'VPC_LINK'] },
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to support both forms in case insensitive way

passThrough: { enum: ['NEVER', 'WHEN_NO_MATCH', 'WHEN_NO_TEMPLATES'] },
schema: {
type: 'object',
additionalProperties: { type: ['object', 'string'] },
Copy link
Contributor

Choose a reason for hiding this comment

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

On AWS side it's type Json, does it mean that both forms are accepted, object and string ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I got confused by the example:

functions:
  create:
    handler: posts.create
    events:
      - http:
          path: posts/create
          method: post
          request:
            schema:
              application/json: ${file(create_request.json)}

I understand now that ${file(create_request.json)} resolves to json (as object) so the string option is not relevant

Comment on lines 89 to 99
oneOf: [
{ properties: { origin: { type: 'string' } } },
{
properties: {
origins: {
type: 'array',
items: { type: 'string' },
},
},
},
],
Copy link
Contributor

Choose a reason for hiding this comment

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

As I test locally, in this form schema will in most cases fail, we've used oneOf, and no property listed is required (so in most cases both variants match, and that's invalid by schema).

I guess it's not exposed by tests, only because there are no tests that involve this event written with runServerless

Normally we configure such things, by defining involved properties normally in properties section , and use oneOf to configure two different required collections

@codecov-io
Copy link

codecov-io commented Oct 7, 2020

Codecov Report

Merging #8301 into master will increase coverage by 0.01%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #8301      +/-   ##
==========================================
+ Coverage   88.01%   88.03%   +0.01%     
==========================================
  Files         249      249              
  Lines        9307     9267      -40     
==========================================
- Hits         8192     8158      -34     
+ Misses       1115     1109       -6     
Impacted Files Coverage Δ
...ins/aws/package/compile/events/apiGateway/index.js 100.00% <100.00%> (ø)
.../aws/package/compile/events/apiGateway/lib/cors.js 100.00% <100.00%> (ø)
.../package/compile/events/apiGateway/lib/validate.js 96.23% <100.00%> (-0.88%) ⬇️
lib/plugins/aws/deployFunction/index.js 89.56% <0.00%> (-0.71%) ⬇️
...ins/aws/customResources/resources/s3/lib/bucket.js 0.00% <0.00%> (ø)
...ws/customResources/resources/s3/lib/permissions.js 0.00% <0.00%> (ø)
...Resources/resources/eventBridge/lib/eventBridge.js 0.00% <0.00%> (ø)
...Resources/resources/eventBridge/lib/permissions.js 0.00% <0.00%> (ø)
...esources/resources/cognitoUserPool/lib/userPool.js 0.00% <0.00%> (ø)
...urces/resources/cognitoUserPool/lib/permissions.js 0.00% <0.00%> (ø)
... and 4 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d2fb696...912ab8c. Read the comment docs.

Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

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

Thank you @thewizarodofoz! We're closer! :)

async: { type: 'boolean' },
authorizer: authorizerSchema,
connectionId: { type: 'string' },
connectionType: { enum: ['vpc-link', 'VPC_LINK'].map(caseInsensitive) },
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this produces as schema, where we expect an objects with regexp properties.

I believe this should be converted to anyOf list

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used oneOf to be consistent with other places that have .map(caseInsensitive). The array will have distinct patterns, so a value can never match more than one option.

type: 'object',
propertyNames: {
type: 'string',
pattern: '\\d{3}',
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to wrap it with ^ and $

},
template: {
type: 'object',
additionalProperties: { type: ['string', 'null'] },
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's not distinguish null in schema.

We actually support null at every property (it's treated to be equivalent to undefined or not set at all)

Adding extra recognition is specific schema is not effective in any way

items: { type: 'string' },
},
},
oneOf: [{ required: ['origin'] }, { required: ['origins'] }],
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to also introduce required: [], otherwise we make passing origin required (either via origin or origins option)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems to me from the code that indeed one of them is required. We also had this manual validation that I removed which supports my theory.

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, it appears that if someone uses an object form, then not providing origin will fail.

Still that looks as apparent design bug, as by supporting cors: true we indicate that all properties have defaults. Intuitively object form should fall into origin: '*' if one was not set (it's e.g. the case in HTTP API, which has similar cors setting)

Anyway this is side issue (not a concern of this PR) so yes let's leave it as required.

Copy link
Contributor

@medikoo medikoo left a comment

Choose a reason for hiding this comment

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

Thank you @thewizarodofoz it's great to have that in! Thanks for all the patience, that was tricky, as we learned this event properties set is large as has a lot quirks

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.

None yet

4 participants