Skip to content

Commit

Permalink
[Security Solution] Support bundling ESS and Serverless OAS separately (
Browse files Browse the repository at this point in the history
#184348)

**Resolves: elastic/security-team#9516

## Summary

As a part of Serverless API reference documentation effort we need to have an ability to produce independent Serverless and ESS OpenAPI specification (OAS) bundles. This PR addresses this issue by adding a new custom property `x-labels` (applicable to OAS operation objects) representing an array of strings and bundling configuration option to exclude anything marked with specific labels.

## How does it work?

Added functionality allows to mark **OAS operation object** (objects defined under an API endpoint path as a HTTP method like `get`, `post` and etc) with arbitrary labels by using `x-labels` custom property like in an example below

```yaml
paths:
  /api/some_path:
    get:
      x-labels:
        - label1
        - label2
```

This labelling **DOESN'T** change produced bundle by itself. It's required to use bundler's `includeLabels` option to include API endpoint operation object(s). `includeLabels` accepts a list of labels. An operation object is included when it has a label matching labels passed to `includeLabels`. In mathematical terms operation object's labels set intersects with `includeLabels`.
 
## How to use it for producing separate Serverless and ESS bundles?

- Mark OAS operation objects (HTTP methods like `get` or `post`) with `x-labels` custom property.

An example below has all operation objects under `/api/some_path` path labeled with `ess` label as well as operation objects under `/api/another_path` path. On top of that `GET /api/another_path` has `serverless` label as well.

```yaml
...
paths:
  /api/some_path:
    get:
      x-labels: [ess]
      ...
    post:
      x-labels: [ess]
      ...
  /api/another_path:
    get:
      x-labels: [ess, serverless]
      ...
    post:
      x-labels: [ess]
      ...
...
```

- Configure bundler with bundling options to include specific labels. `options.includeLabels` is responsible for including document nodes labeled with specific labels. You need two bundler invocations with different `options.includeLabels` values like below

```js
bundle({ // (1)
  ...
  options: {
    includeLabels: ['serverless'],
  },
});

bundle({ // (2)
  ...
  options: {
    includeLabels: ['ess'],
  },
});
```

It will produce two following bundles

(1) for Serverless

```yaml
...
paths:
  /api/another_path:
    get:
      ...
...
```

and (2) for ESS
```yaml
...
paths:
  /api/some_path:
    get:
      ...
    post:
      ...
  /api/another_path:
    get:
      ...
    post:
      ...
...
```

You may notice (2) has everything included since each operation object is labeled with `ess` label.
  • Loading branch information
maximpn authored Jun 7, 2024
1 parent 15b6ba9 commit 2755b89
Show file tree
Hide file tree
Showing 62 changed files with 1,645 additions and 502 deletions.
269 changes: 233 additions & 36 deletions packages/kbn-openapi-bundler/README.md

Large diffs are not rendered by default.

53 changes: 35 additions & 18 deletions packages/kbn-openapi-bundler/src/bundler/bundle_document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@
*/

import { isAbsolute } from 'path';
import { RefResolver } from './ref_resolver';
import { processDocument } from './process_document';
import { BundleRefProcessor } from './document_processors/bundle_refs';
import { createSkipNodeWithInternalPropProcessor } from './document_processors/skip_node_with_internal_prop';
import { createModifyPartialProcessor } from './document_processors/modify_partial';
import { createSkipInternalPathProcessor } from './document_processors/skip_internal_path';
import { ResolvedDocument, ResolvedRef } from './types';
import { createRemovePropsProcessor } from './document_processors/remove_props';
import { createModifyRequiredProcessor } from './document_processors/modify_required';
import { X_CODEGEN_ENABLED, X_INLINE, X_INTERNAL, X_MODIFY } from './known_custom_props';
import { RemoveUnusedComponentsProcessor } from './document_processors/remove_unused_components';
import { RefResolver } from './ref_resolver/ref_resolver';
import { processDocument } from './process_document/process_document';
import { X_CODEGEN_ENABLED, X_INLINE, X_INTERNAL, X_LABELS, X_MODIFY } from './known_custom_props';
import { isPlainObjectType } from '../utils/is_plain_object_type';
import { ResolvedDocument } from './ref_resolver/resolved_document';
import { ResolvedRef } from './ref_resolver/resolved_ref';
import { createSkipNodeWithInternalPropProcessor } from './process_document/document_processors/skip_node_with_internal_prop';
import { createSkipInternalPathProcessor } from './process_document/document_processors/skip_internal_path';
import { createModifyPartialProcessor } from './process_document/document_processors/modify_partial';
import { createModifyRequiredProcessor } from './process_document/document_processors/modify_required';
import { createRemovePropsProcessor } from './process_document/document_processors/remove_props';
import {
createFlattenFoldedAllOfItemsProcessor,
createMergeNonConflictingAllOfItemsProcessor,
createUnfoldSingleAllOfItemProcessor,
} from './document_processors/reduce_all_of_items';
} from './process_document/document_processors/reduce_all_of_items';
import { createIncludeLabelsProcessor } from './process_document/document_processors/include_labels';
import { BundleRefProcessor } from './process_document/document_processors/bundle_refs';
import { RemoveUnusedComponentsProcessor } from './process_document/document_processors/remove_unused_components';

export class SkipException extends Error {
constructor(public documentPath: string, message: string) {
Expand All @@ -35,6 +37,10 @@ export interface BundledDocument extends ResolvedDocument {
bundledRefs: ResolvedRef[];
}

interface BundleDocumentOptions {
includeLabels?: string[];
}

/**
* Bundles document into one file and performs appropriate document modifications.
*
Expand All @@ -49,7 +55,10 @@ export interface BundledDocument extends ResolvedDocument {
* @param absoluteDocumentPath document's absolute path
* @returns bundled document
*/
export async function bundleDocument(absoluteDocumentPath: string): Promise<BundledDocument> {
export async function bundleDocument(
absoluteDocumentPath: string,
options?: BundleDocumentOptions
): Promise<BundledDocument> {
if (!isAbsolute(absoluteDocumentPath)) {
throw new Error(
`bundleDocument expects an absolute document path but got "${absoluteDocumentPath}"`
Expand All @@ -69,18 +78,26 @@ export async function bundleDocument(absoluteDocumentPath: string): Promise<Bund
throw new SkipException(resolvedDocument.absolutePath, 'Document has no paths defined');
}

const bundleRefsProcessor = new BundleRefProcessor(X_INLINE);
const removeUnusedComponentsProcessor = new RemoveUnusedComponentsProcessor();

await processDocument(resolvedDocument, refResolver, [
const defaultProcessors = [
createSkipNodeWithInternalPropProcessor(X_INTERNAL),
createSkipInternalPathProcessor('/internal'),
createModifyPartialProcessor(),
createModifyRequiredProcessor(),
createRemovePropsProcessor([X_INLINE, X_MODIFY, X_CODEGEN_ENABLED]),
createRemovePropsProcessor([X_INLINE, X_MODIFY, X_CODEGEN_ENABLED, X_LABELS]),
createFlattenFoldedAllOfItemsProcessor(),
createMergeNonConflictingAllOfItemsProcessor(),
createUnfoldSingleAllOfItemProcessor(),
];

if (options?.includeLabels) {
defaultProcessors.push(createIncludeLabelsProcessor(options?.includeLabels));
}

const bundleRefsProcessor = new BundleRefProcessor(X_INLINE);
const removeUnusedComponentsProcessor = new RemoveUnusedComponentsProcessor();

await processDocument(resolvedDocument, refResolver, [
...defaultProcessors,
bundleRefsProcessor,
removeUnusedComponentsProcessor,
]);
Expand Down
12 changes: 12 additions & 0 deletions packages/kbn-openapi-bundler/src/bundler/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/**
* Document abstraction. We don't mind OpenAPI `3.0` and `3.1` differences.
*/
export type Document = Record<string, unknown>;
12 changes: 10 additions & 2 deletions packages/kbn-openapi-bundler/src/bundler/known_custom_props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@

/**
* `x-internal: true` marks nodes the bundler must NOT include in the result bundled document. Any other values are ignored.
* See README for more details.
*/
export const X_INTERNAL = 'x-internal';

/**
* `x-internal: true` marks reference nodes the bundler must inline in the result bundled document.
* `x-inline: true` marks reference nodes the bundler must inline in the result bundled document.
* See README for more details.
*/
export const X_INLINE = 'x-inline';

/**
* `x-modify` marks nodes to be modified by the bundler. `partial` and `required` values are supported.
* `x-modify` marks nodes to be modified by the bundler. `partial` and `required` values are supported. See README for more details.
*
* - `partial` leads to removing `required` property making params under `properties` optional
* - `required` leads to adding or extending `required` property by adding all param names under `properties`
Expand All @@ -29,3 +31,9 @@ export const X_MODIFY = 'x-modify';
* in result bundled document.
*/
export const X_CODEGEN_ENABLED = 'x-codegen-enabled';

/**
* `x-labels` allows to mark operation objects with arbitrary labels. It allows to exclude or include nodes
* marked with specific labels into the resulting bundle. See README for more details.
*/
export const X_LABELS = 'x-labels';
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { OpenAPIV3 } from 'openapi-types';
import deepEqual from 'fast-deep-equal';
import chalk from 'chalk';
import { insertRefByPointer } from '../../utils/insert_by_json_pointer';
import { ResolvedRef } from '../types';
import { ResolvedRef } from '../ref_resolver/resolved_ref';
import { BundledDocument } from '../bundle_document';

export function mergeSharedComponents(
Expand Down
230 changes: 0 additions & 230 deletions packages/kbn-openapi-bundler/src/bundler/process_document.test.ts

This file was deleted.

Loading

0 comments on commit 2755b89

Please sign in to comment.