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

[HTTP] Versioned router implementation #153543

Merged
merged 43 commits into from
Mar 28, 2023

Conversation

jloleysens
Copy link
Contributor

@jloleysens jloleysens commented Mar 23, 2023

Summary

Implements the designs from #151596

  • Move packages/versioning/* into packages/core/http to follow existing structure more closely
  • Implements the first iteration of the versioned router as a wrapper/layer around the existing router
  • Adds some integration tests
  • Future work needed! Once we have a the versioned spec we should implement it in this wrapper layer
  • Validation is a little bit tricky because of when the CoreKibanaResponse object is instantiated, the approach taken here is to replace body, params, query on the route-level's request object

Closes #149286

Next steps:

  1. Expose this versioned router on the IRouter interface
  2. Pass through flag for dev-only checks
  3. Implement the client side changes

@jloleysens jloleysens added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc release_note:skip Skip the PR/issue when compiling release notes Epic:VersionedAPIs Kibana Versioned APIs v8.8.0 labels Mar 23, 2023
// This validation is a pass-through so that we can apply our version-specific validation later
const passThroughValidation = { body: schema.any(), params: schema.any(), query: schema.any() };

export class InternalVersionedRoute implements VersionedRoute {
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 expect most of the code in here will be implementing the actual API specification and making sure versioned routes adhere to it.

*
* @experimental
*/
export interface VersionHTTPToolkit {
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 decided to remove the concept of "Toolkit" as I don't think it adds anything.

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

Nothing major to report, but a bunch of NITs, remarks and questions

Comment on lines 117 to 131
if (this.validateResponses && handler.options.validate && handler.options.validate.response) {
const { response } = handler.options.validate;
try {
validate(
req,
{ body: response.body, unsafe: { body: response.unsafe } },
handler.options.version
);
} catch (e) {
return res.custom({
statusCode: 500,
body: `Failed output validation: ${e.message}`,
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

:oh_no_you_didnt:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😄 gotta love structural typing

.github/CODEOWNERS Outdated Show resolved Hide resolved
@jloleysens jloleysens requested a review from a team as a code owner March 23, 2023 17:15
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)


const validation = handler.options.validate || undefined;

const mutableCoreKibanaRequest = req as Mutable<CoreKibanaRequest>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(moving comment from previous location)

This is so that we can hide/remove unvalidated inputs after CoreKibanaRequest was constructed. Not super happy about this, but it seemed like the least trouble without doing one of:

(1) Exposing more knowledge of Router internals so that we can dynamically set validation schema (right now we just use IRouter)
(2) Changing our approach, integrating the versioned router functionality into the Router implementation more directly

}

/** @experimental */
export type RequestValidation<P, Q, B> = RouteValidatorFullConfig<P, Q, B>;

/** @experimental */
export interface ResponseValidation<R> {
body: RouteValidationFunction<R> | Type<R>;
[statusCode: number]: { body: RouteValidationFunction<R> | Type<R> };
Copy link
Contributor Author

@jloleysens jloleysens Mar 24, 2023

Choose a reason for hiding this comment

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

Following OAS's example here. For the responses we added validation for specific status codes. At the moment this is used to scope validation to a specific status code, but we could go further and throw if an unknown status code was provided.

Note: output validation is intended to run in dev only.

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

Implementation looks good to me.

What are the next steps / what are we missing to effectively expose this feature to our consumers via Core APIs?

Comment on lines 132 to 141
export type {
AddVersionOpts,
RequestValidation,
ResponseValidation,
Version,
VersionedRoute,
VersionedRouteConfig,
VersionedRouteRegistrar,
VersionedRouter,
} from './src/versioning';
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: I wonder if we shouldn't add a more explicit prefix to some of those types, just to make it more understandable that they're bound to the versioned router. Mostly thinking about:

  • Version
  • RequestValidation
  • ResponseValidation

Maybe (I know, those are long names...):

  • RouteVersion (or ApiVersion?)
  • VersionedRouteRequestValidation
  • VersionedRouteResponseValidation

const router = {} as unknown as IRouter<MyCustomContext>;

const versionedRouter = vtk.createVersionedRouter({ router });
const versionedRouter = {} as unknown as VersionedRouter<MyCustomContext>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that we have an implementation, should we get rid of this file, or convert it to documentation or TSDoc?

Comment on lines +65 to +66
// TODO: Make "true" dev-only
private readonly validateResponses: boolean = true
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it a TODO for now or a TODO for later 😄?

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 was planning on addressing this when exposing the versioned router on our http router instance :thu:

@jloleysens jloleysens enabled auto-merge (squash) March 28, 2023 08:59
@jloleysens
Copy link
Contributor Author

What are the next steps / what are we missing to effectively expose this feature to our consumers via Core APIs?

I'll update the PR description with this info @pgayvallet

@kibana-ci
Copy link
Collaborator

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/core-http-server 161 176 +15
@kbn/core-http-versioned-router-server-internal - 15 +15
total +30

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
@kbn/core-http-versioned-router-server-internal - 2 +2
Unknown metric groups

API count

id before after diff
@kbn/core-http-server 413 439 +26
@kbn/core-http-versioned-router-server-internal - 15 +15
total +41

ESLint disabled line counts

id before after diff
securitySolution 433 436 +3

Total ESLint disabled count

id before after diff
securitySolution 513 516 +3

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@jloleysens jloleysens merged commit e8055e8 into elastic:main Mar 28, 2023
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label Mar 28, 2023
Copy link
Member

@afharo afharo left a comment

Choose a reason for hiding this comment

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

What a great effort! This is exciting! LGTM!

I added 2 comments for your consideration. But feel free to tackle them in the upcoming follow ups.

AddVersionOpts,
VersionedRoute,
VersionedRouteConfig,
} from '@kbn/core-http-server';
Copy link
Member

Choose a reason for hiding this comment

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

Super-nit: 2 imports from the same package can be merged

validation.request,
handler.options.version
);
mutableCoreKibanaRequest.body = body;
Copy link
Member

Choose a reason for hiding this comment

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

I think this will also mutate req. Do we want to use clone?

@jloleysens jloleysens deleted the versioned-router-impl branch March 28, 2023 10:35
jloleysens added a commit that referenced this pull request Apr 3, 2023
## Summary

Now that we merged #153543, this
PR exposes the versioned router for teams to start using. The versioned
router will be available on `IRouter` under a new `versioned` property.

Primary benefit of this approach is that plugin developers will not need
to do anything other than "get" the `versioned` property to get a
versioned router.

Drawback is that this precludes us from passing in additional
configuration, like a version, to scope the versioned router instance.
For that we would need some kind of `createVersionedRouter({ version:
... })`. At this point it is not clear this is necessary, we could
revisit this decision based on actual usage. Plugin developers could
also do something like:

```ts
// common const
const MY_API_VERSION: ApiVersion = '1';

// in routes
import {MY_API_VERSION} from '../from/common';
router.versioned.get({ path: ... })
  .addVersion({ version: MY_API_VERSION });
```

In this way they could get many of the same benefits of a version-scoped
version router, with the drawback that they need to pass this in for
every route.

### TODO

- [x] Add an integration test for the versioned router

### Future work

* We still need to consider revisiting some of the router design to
better support internal cases like adding support for registering a
handler for a version range and adding a default version to continue
supporting on-prem where introducing versions will be a breaking change

---------

Co-authored-by: kibanamachine <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Epic:VersionedAPIs Kibana Versioned APIs release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.8.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Versioned API Router
6 participants