kibana/packages/kbn-openapi-bundler
Maxim Palenov e3d95e9b72
[Security Solution] OpenAPI docs bundler (#171526)
**Addresses:** https://github.com/elastic/security-team/issues/7981

## Summary

This PR adds an OpenAPI spec bundler to simplify integration with the
Docs Engineering team. The bundler produces a single bundled file by
dereferencing and inlining some of external references and bundling them
and paths into a single file.

## Details

Currently we maintain a number of schema files inside
`x-pack/plugins/security_solution/common/api/**.schema.yaml` and it
might be hard for external teams to keep track of all the changes in our
schemas. By creating a singular schema file, we provide a clear
integration point for others.

The bundler addresses the following issues

- hide endpoints that we don't want to expose (Endpoints related to
features hidden under a feature flag and all internal endpoints should
be excluded from the file)
- hide not finished data structures related to features hidden under a
feature flag or data structures that are not designed to be public (For
example `RuleActionAlertsFilter` or `RuleActionParams` are exposed
directly from the Alerting framework and might be considered
implementation details, we don't want to document interfaces that are
not designed to be public so hiding them is a good option)
- modify spec based on presence of `x-modify` property (Instead of
exposing `x-modify: partial` we need to make the exported data structure
partial and instead of exposing `x-modify: required` we need to make the
exported data structure required)
- remove any internal attributes used for code generation like
`x-codegen-enabled` and `x-modify`
- inline some of the reused data structures (We have a lot of low-level
reusable data structures `in common_attributes.schema.yaml` which might
make the final documentation hardly usable from the UX perspective, so
we can inline them)

and lives in a new `@kbn/openapi-bundler` package under
`packages/kbn-openapi-bundler` folder.

### Related changes

- Implicit version type `version: 2023-10-31` has been changed to
explicit string type `version: '2023-10-31'` for all specs under
`security_solution/common/api` folder. Implicit type causes `js-yaml`
parsing it as a `Data` JS object leading to serializing it like
`2023-10-31T00:00:00.000Z`.
- `ListRequestQuery` schema in
`security_solution/common/api/endpoint/actions/list.schema.yaml ` has
been renamed to `EndpointActionListRequestQuery` to avoid conflicts with
`ListRequestQuery` in
`security_solution/common/api/endpoint/metadata/list_metadata.schema.yaml`.
While it's not an issue to have completely different schemas sharing the
same name in different files it may be an indication of pitfalls in the
API design. I'd say it's an open question if such cases need to be
always resolved automatically or reviewed manually. At this moment the
bundler can't resolve such conflicts.

## How to test?

There is a a new JS script added to Security Solution plugin located at
`x-pack/plugins/security_solution/scripts/openapi/bundle.js` with a
corresponding entry in `package.json` named `openapi:bundle`.

To test the PR change directory to Security Solution plugin's root
folder and run the bundler like below

```sh
cd x-pack/plugins/security_solution
yarn openapi:bundle
```

It should produce a bundled OpenAPI spec at
`x-pack/plugins/security_solution/target/openapi/security_solution.bundled.schema.yaml`.

## Open issues

- [x] Circular references (implemented in
dfdf0a51ea)
- [x] Mix of OpenAPI `3.0` and `3.1` specs (Maybe convert automatically
to `3.1`?). Folder like OpenAPI bundling format implemented
[here](0ae7ad5abb)
allows to mix OpenAPI `3.0` and `3.1` specs.

## Improvements

- [ ] Flexible configuration
- [ ] CLI support?

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
2023-11-29 14:07:31 +01:00
..
src [Security Solution] OpenAPI docs bundler (#171526) 2023-11-29 14:07:31 +01:00
index.ts [Security Solution] OpenAPI docs bundler (#171526) 2023-11-29 14:07:31 +01:00
jest.config.js [Security Solution] OpenAPI docs bundler (#171526) 2023-11-29 14:07:31 +01:00
kibana.jsonc [Security Solution] OpenAPI docs bundler (#171526) 2023-11-29 14:07:31 +01:00
package.json [Security Solution] OpenAPI docs bundler (#171526) 2023-11-29 14:07:31 +01:00
README.md [Security Solution] OpenAPI docs bundler (#171526) 2023-11-29 14:07:31 +01:00
tsconfig.json [Security Solution] OpenAPI docs bundler (#171526) 2023-11-29 14:07:31 +01:00

OpenAPI Specs Bundler for Kibana

@kbn/openapi-bundler is a tool for transforming multiple OpenAPI specification files (source specs) into a single bundled specification file (target spec). This can be used for API docs generation purposes. This approach allows you to:

  • Abstract away the knowledge of where you keep your OpenAPI specs, how many specs there are, and how to find them. The Docs team should only know where a single file is located - the bundle.
  • Omit internal API endpoints from the bundle.
  • Omit API endpoints that are hidden behind a feature flag and haven't been released yet.
  • Omit parts of schemas that are hidden behind a feature flag (e.g. a new property added to an existing response schema).
  • Omit custom OpenAPI attributes from the bundle, such as x-codegen-enabled, x-internal, and x-modify (see below).
  • Transform the target schema according to the custom OpenAPI attributes, such as x-modify.
  • Resolve references and inline some of them for better readability. The bundled file contains only local references and paths.

Getting started

To let this package help you with bundling your OpenAPI specifications you should have OpenAPI specification describing your API endpoint request and response schemas along with common types used in your API. Refer @kbn/openapi-generator and OpenAPI 3.0.3 (support for OpenAPI 3.1.0 is planned to be added soon) for more details.

Following recommendations provided in @kbn/openapi-generator you should have OpenAPI specs defined under a common folder something like my-plugin/common/api.

Currently package supports only programmatic API. As the next step you need to create a JavaScript script file like below and put it to my-plugin/scripts/openapi

require('../../../../../src/setup_node_env');
const { bundle } = require('@kbn/openapi-bundler');
const { resolve } = require('path');

// define ROOT as `my-plugin` instead of `my-plugin/scripts/openapi`
// pay attention to this constant when your script's location is different
const ROOT = resolve(__dirname, '../..');

bundle({
  rootDir: ROOT, // Root path e.g. plugin root directory
  sourceGlob: './**/*.schema.yaml', // Glob pattern to find OpenAPI specification files
  outputFilePath: './target/openapi/my-plugin.bundled.schema.yaml', //
});

And add a script entry to your package.json file

{
  "author": "Elastic",
  ...
  "scripts": {
    ...
    "openapi:bundle": "node scripts/openapi/bundle"
  }
}

Finally you should be able to run OpenAPI bundler via

yarn openapi:bundle

This command will produce a bundled file my-plugin/target/openapi/my-plugin.bundled.schema.yaml containing all specs matching ./**/*.schema.yaml glob pattern.

Here's an example how your source schemas can look like and the expected result

  • example1.schema.yaml
openapi: 3.0.3
info:
  title: My endpoint
  version: '2023-10-31'

paths:
  /api/path/to/endpoint:
    get:
      operationId: MyGetEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
  • example2.schema.yaml
openapi: 3.0.3
info:
  title: My endpoint
  version: '2023-10-31'

paths:
  /api/path/to/endpoint:
    post:
      x-internal: true
      operationId: MyPostEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

And the target spec will look like

openapi: 3.0.3
info:
  title: Bundled specs file. See individual paths.verb.tags for details
  version: not applicable
paths:
  /api/path/to/endpoint:
    get:
      operationId: MyGetEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
    post:
      operationId: MyPostEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
components:
  schemas: {}

Supported custom (x- prefixed) properties

OpenAPI specification allows to define custom properties. They can be used to describe extra functionality that is not covered by the standard OpenAPI Specification. We currently support the following custom properties

  • x-internal - marks source spec nodes the bundler must NOT include in the target spec
  • x-modify - marks nodes to be modified by the bundler
  • x-inline - marks reference nodes to be inlined when bundled

x-internal

Marks source spec nodes the bundler must NOT include in the target spec.

Supported values: true

When bundler encounters a node with x-internal: true it doesn't include this node into the target spec. It's useful when it's necessary to hide some chunk of OpenAPI spec because functionality supporting it is hidden under a feature flag or the chunk is just for internal use.

Examples

The following spec defines an API endpoint /api/path/to/endpoint accepting GET and POST requests. It has x-internal: true defined in post section meaning it won't be included in the target spec.

openapi: 3.0.3
info:
  title: My endpoint
  version: '2023-10-31'

paths:
  /api/path/to/endpoint:
    get:
      operationId: MyGetEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
    post:
      x-internal: true
      operationId: MyPostEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

The target spec will look like

openapi: 3.0.3
info:
  title: Bundled specs file. See individual paths.verb.tags for details
  version: not applicable
paths:
  /api/path/to/endpoint:
    get:
      operationId: MyGetEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

x-internal: true can also be defined next to a reference.

openapi: 3.0.3
info:
  title: My endpoint
  version: '2023-10-31'

paths:
  /api/path/to/endpoint:
    get:
      operationId: MyGetEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
    post:
      $ref: '#/components/schemas/MyPostEndpointResponse'
      x-internal: true

components:
  schemas:
    MyPostEndpointResponse:
      operationId: MyPostEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

The target spec will look like

openapi: 3.0.3
info:
  title: Bundled specs file. See individual paths.verb.tags for details
  version: not applicable
paths:
  /api/path/to/endpoint:
    get:
      operationId: MyGetEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

x-modify

Marks nodes to be modified by the bundler.

Supported values: partial or required

Value partial leads to removing required property making params under properties optional. Value required leads to adding or extending required property by adding all param names under properties.

Examples

The following spec has x-modify: partial at schema section. It makes params optional for a PATCH request.

openapi: 3.0.0
info:
  title: My endpoint
  version: '2023-10-31'
paths:
  /api/path/to/endpoint:
    patch:
      operationId: MyPatchEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              x-modify: partial
              type: object
              properties:
                param1:
                  type: string
                  enum: [val1, val2, val3]
                param2:
                  type: number
              required:
                - param1
                - param2

The target spec will look like

openapi: 3.0.0
info:
  title: Bundled specs file. See individual paths.verb.tags for details
  version: not applicable
paths:
  /api/path/to/endpoint:
    patch:
      operationId: MyPatchEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                param1:
                  type: string
                  enum: [val1, val2, val3]
                param2:
                  type: number

The following spec has x-modify: required at schema section. It makes params optional for a PATCH request.

openapi: 3.0.0
info:
  title: My endpoint
  version: '2023-10-31'
paths:
  /api/path/to/endpoint:
    put:
      operationId: MyPutEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              x-modify: required
              type: object
              properties:
                param1:
                  type: string
                  enum: [val1, val2, val3]
                param2:
                  type: number

The target spec will look like

openapi: 3.0.0
info:
  title: Bundled specs file. See individual paths.verb.tags for details
  version: not applicable
paths:
  /api/path/to/endpoint:
    patch:
      operationId: MyPatchEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                param1:
                  type: string
                  enum: [val1, val2, val3]
                param2:
                  type: number
              required:
                - param1
                - param2

x-modify can also be defined next to a reference.

openapi: 3.0.0
info:
  title: My endpoint
  version: '2023-10-31'
paths:
  /api/path/to/endpoint:
    patch:
      operationId: MyPatchEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchProps'
              x-modify: partial

components:
  schemas:
    PatchProps:
      type: object
      properties:
        param1:
          type: string
          enum: [val1, val2, val3]
        param2:
          type: number
      required:
        - param1
        - param2

The target spec will look like

openapi: 3.0.0
info:
  title: Bundled specs file. See individual paths.verb.tags for details
  version: not applicable
paths:
  /api/path/to/endpoint:
    patch:
      operationId: MyPatchEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                param1:
                  type: string
                  enum: [val1, val2, val3]
                param2:
                  type: number

x-inline

Marks reference nodes to be inlined when bundled.

Supported values: true

x-inline: true can be specified at a reference node itself (a node with $ref key) or at a node $ref resolves to. When bundler encounters such a node it assigns (copies keys via Object.assign()) the latter node (a node$ref resolves to) to the first node (a node with $ref key). This way target won't have referenced component in components as well.

Examples

The following spec defines an API endpoint /api/path/to/endpoint accepting POST request. It has x-inline: true specified in post section meaning reference #/components/schemas/MyPostEndpointResponse will be inlined in the target spec.

openapi: 3.0.3
info:
  title: My endpoint
  version: '2023-10-31'

paths:
  /api/path/to/endpoint:
    post:
      $ref: '#/components/schemas/MyPostEndpointResponse'
      x-inline: true

components:
  schemas:
    MyPostEndpointResponse:
      operationId: MyPostEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

The target spec will look like

openapi: 3.0.3
info:
  title: Bundled specs file. See individual paths.verb.tags for details
  version: not applicable
paths:
  /api/path/to/endpoint:
    post:
      operationId: MyPostEndpoint
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object