加载中

为 HTTP API 生成 OAS

注意:所有公共路由均需提供 OAS

如果您的路由声明了 access: 'public',则必须为其提供最新的 OpenAPI 规范。这些路由的文档托管在我们的文档网站上,并用于客户端集成。例如:我们的 Elastic stack terraform provider

警告:设计优秀的代码优先 (code-first) API

代码优先的 API 架构必须经过仔细设计,才能生成清晰的 OpenAPI 3.0 输出。请优先使用简单的 @kbn/config-schema 类型,并保持请求/响应形状(shapes)狭窄且明确。有关如何为 OAS 设计 API 的更多信息,请参阅 HTTP API 设计

基于运行时的复杂架构虽然可以正确验证,但生成的 OAS 可能会令人困惑、有损或不完整。请参阅 <a href="#oas-compatibility-kbn-config-schema-types">无法清晰映射到 OAS 3.0 的类型和模式</a>。

在合并到 main 之前,请务必预览您生成的 OAS。在 /oas_docs 中运行 make help 查看预览命令。

要为 HTTP API 生成 OAS,必须使用以下组件

  1. Core 的 routerrouter.versioned,用于定义通过 core.http 服务提供给所有插件的 HTTP API
  2. @kbn/config-schema@kbn/zod 请求和响应架构
注意:超越运行时验证

Kibana 核心平台支持将 @kbn/config-schema 作为各种架构用途的一等公民:配置、已保存对象(saved objects)以及 HTTP API 请求/响应体。

开发人员可以利用 @kbn/config-schema 作为运行时验证、TypeScript 接口和 OpenAPI 规范的单一事实来源。

kibana.dev.yml 中添加以下配置

server.oas.enabled: true
		

启动 Kibana 并发送以下请求

curl -s -uelastic:changeme https://:5601/api/oas\?pathStartsWith\=/api/foo
		

返回的值应包含您的路由以及任何以 /api/foo 开头的其他路径的 OpenAPI 规范。

其他有用的过滤查询参数包括

  • pluginId - 获取特定插件的 OAS,例如:@kbn/data-views-plugin
  • access - 筛选特定的访问级别:支持 publicinternal

<a id="oas-compatibility-kbn-config-schema-types"></a>

在编写公共 API 时,请将本节作为实用清单。

@kbn/config-schema 类型/模式 为什么这对 OAS 3.0 有问题 首选替代方案
schema.byteSize() / schema.duration() 解析为特定于运行时的值类型(ByteSizeValue, moment.Duration),这些类型对于 OpenAPI 消费者来说不是标准的 JSON 架构基元。 使用 schema.string()(人类可读单位)或 schema.number()(归一化基准单位),并在 meta.description 中记录单位(例如:“以毫秒为单位的持续时间”,“以字节为单位的大小”)。
schema.buffer() / schema.stream() 二进制和流运行时对象无法自然地映射到标准的 JSON 请求/响应体。 将有效负载建模为 JSON 友好的基元/对象。对于二进制传输,请记录媒体类型并使用明确的 OpenAPI 请求/响应内容定义。
schema.any() 这是一个逃生舱(escape hatch),在生成的 OAS 中几乎无法产生有用的契约。 使用一个小的显式 schema.object({...})schema.recordOf(...) 或带有已记录字段的受限联合类型。
schema.mapOf() / schema.recordOf()(当键的形状很重要时) OAS 通常将这些表示为带有 additionalPropertiestype: object,这无法向客户端清晰地传达所有键约束。 如果键是已知的,请建立显式的对象属性。如果键是动态的,请保持值简单,并在描述中记录键的格式。
用于对象变体的 schema.oneOf()(特别是嵌套的 oneOf 没有明确鉴别符(discriminator)的联合类型对于人类/工具来说更难理解。嵌套形式(如 schema.oneOf([schema.oneOf([a, b]), schema.oneOf([c, d])]))特别难以阅读,并会导致糟糕的验证错误。 首选 schema.discriminatedUnion('type', [...]),使用稳定的鉴别符和扁平的变体。
schema.conditional(), schema.contextRef(), schema.siblingRef() 行为取决于运行时上下文,这很难编码为稳定、可移植的 OAS 契约。 首选显式的路由版本或显式的鉴别符/对象形状,以便行为在契约中是静态可见的。
// In server/schemas/v1.ts
import { schema, TypeOf } from '@kbn/config-schema';

export const fooResource = schema.object({
  name: schema.string({
    meta: { description: 'A unique identifier for...' },
  }),
  // ...and any other fields you may need
});

export type FooResource = TypeOf<typeof fooResource>;

// In common/foo/v1.ts
export type { FooResource } from '../server/schemas/v1';

// In common/index.ts expose this as the "latest" schema shape
export type { FooResource } from './latest';

export * as fooResourceV1 from '../foo/v1';
		

此示例演示了如何组织运行时架构以进行准备

  1. 具备版本控制
  2. 让插件中的客户端和服务器代码均可使用 TypeScript 引用

有关此组织模式的更多信息,请参阅 架构版本控制策略

// Somewhere in your plugin's server/routes folder
import { schema, TypeOf } from '@kbn/config-schema';
import type { FooResource } from '../../../common';
import { fooResource } from '../../schemas/v1';

// Note: this response schema is instantiated lazily to avoid creating schemas that are not needed in most cases!
const fooResourceResponse = () => {
  return schema.object({
    id: schema.string({
      maxLength: 20,
      meta: { description: 'Add a description.' }
    }),
    name: schema.string({ meta: { description: 'Add a description.' } }),
    createdAt: schema.string({
      meta: {
        description: 'Add a description.',
        deprecated: true,
      },
    }),
  })
}

// Note: TypeOf can extract types for lazily instantiated schemas
type FooResourceResponse = TypeOf<typeof fooResourceResponse>

function registerFooRoute(router: IRouter, docLinks: DoclinksStart) {
  router.versioned
    .post({
      path: '/api/foo',
      access: 'public',
      summary: 'Create a foo resource'
      description: `A foo resource enables baz. See the following [documentation](${docLinks.links.fooResource}).`,
      deprecated: true,
      options: {
        tags: ['oas-tag:my tag'],
        availability: {
          since: '1.0.0',
          stability: 'experimental',
        },
      },
    })
    .addVersion({
      version: '2023-10-31',
      validate: {
        request: {
          body: fooResource,
        },
        response: {
          200: {
            description: 'Indicates a successful call.',
            body: fooResourceResponse,
          },
        },
      },
    },
    async (ctx, req, res) => {
      const core = await ctx.core;
      const savedObjectsClient = core.savedObjects.client;
      const body = req.body;
      const foo = await createFoo({ name: body.name });
      // This is our HTTP translation layer to ensure only the necessary fields included
      const responseBody: FooResourceResponse = {
        id: foo.id,
        name: foo.name,
        createdAt: foo.createdAt,
      };
      return res.ok({ body: responseBody });
    }
  );
}
		
  1. 表示该属性已弃用的指示器
  2. 表示该操作已弃用的指示器
  3. 每个操作必须有一个标签,用于在文档中对类似的端点进行分组
  4. API 添加的版本。
  5. 当前的生命周期:实验性 (experimental)、测试版 (beta) 或稳定版 (stable)

除了请求和响应的架构外,提供具体的请求和响应作为示例非常有用。示例不仅限于默认值,还能让最终用户更直观地了解 API 的行为。有关示例如何向最终用户展示的更多信息,请参阅 bump.sh 文档

要为您创建的上述端点添加示例,您可以执行以下操作

// ...
    .addVersion({
      version: '2023-10-31',
      options: {
        // Be sure and lazily instantiate this value. It's only used at dev time!
        oasOperationObject: () => ({
          requestBody: {
            content: {
              'application/json': {
                examples: {
                  fooExample1: {
                    summary: 'An example foo request',
                    value: {
                      name: 'Cool foo!',
                    } as FooResource,
                  },
                },
              },
            },
          },
          responses: {
            200: {
              content: {
                'application/json': {
                  examples: {
                    /* Put your 200 response examples here */
                  },
                },
              },
            },
          },
        }),
      },
      validate: {
        request: {
          body: fooResource,
        },
        response: {
          200: {
            body: fooResourceResponse,
          },
        },
      },
    },
// ...
		

这种方法的好处是,您的示例包含在代码中,并且在开发时会进行类型检查。因此,任何形状错误都应在您编写时被捕获。

<details>

<summary>我有现有的基于 YAML 的示例想使用!</summary>

如果您有现有的 YAML 格式示例,并且想要使用,请采用以下方法

import path from 'node:path';

const oasOperationObject: () => path.join(__dirname, 'foo.examples.yaml'),

// ...
    .addVersion({
      version: '2023-10-31',
      options: {
        oasOperationObject,
      },
      validate: {
        request: {
          body: fooResource,
        },
        response: {
          200: {
            body: fooResourceResponse,
          },
        },
      },
    },
// ...
		

其中 foo.examples.yaml 的内容为

requestBody:
  content:
    application/json:
      examples:
        fooExample:
          summary: Foo example
          description: >
            An example request of creating foo.
          value:
            name: 'Cool foo!'
        fooExampleRef:
          # You can use JSONSchema $refs to organize this file further
          $ref: "./examples/foo_example_i_factored_out_of_this_file.yaml"
responses:
  200:
    content:
      application/json:
        examples:
          # Apply a similar pattern to writing examples here
x-codeSamples:
- lang: cURL
  # label: A label which will be used as a title. Defaults to the lang value.
  source: |
    curl \
      -X POST /api/foo
      -H "kbn-xsrf: true"
      -d '{...}'
- lang: Console
  source: |
    POST kbn:/api/agent_builder/tools
    {...}
		
  1. 请确保使用 examples 数组,example(单数)已被弃用

</details>

请参阅 <a href="#how-do-i-see-my-http-apis-oas">此部分</a>,了解如何查看 HTTP API 的 OAS。

从这里开始,您可以迭代开发您的路由和架构定义。每次更改后,Kibana 服务器都会自动重新加载,最新的 OAS 应反映您代码的当前状态!

例如,让我们向架构成员添加一些描述

const fooResourceResponse = () => {
  return schema.object({
    id: schema.string({ maxLength: 20, meta: { description: 'An unique ID for a foo resource.'} }),
    name: schema.string({ meta: { description: 'A human friendly name for a foo resource.'} }),
    createdAt: schema.string({ meta: { description: 'The ISO date a foo resource was created.'} }),
  })
}
		

这些描述现在应该反映在为您路由生成的 OAS 中。

您还可以在代码优先的架构中为各个字段附加可用性元数据。在 @kbn/config-schema 中,使用 meta.availability;在 Zod v4 (@kbn/zod/v4) 中,使用 .meta({ openapi: { availability: ... } })。OpenAPI 生成器将其映射到相应架构属性(或命名组件)上的 x-state 扩展。

// @kbn/config-schema
schema.string({
  meta: {
    description: 'Add a description.',
    availability: { stability: 'stable', since: '9.4.0' },
  },
});
		
// @kbn/zod/v4
import { z } from '@kbn/zod/v4';

z.string().meta({
  openapi: {
    availability: { stability: 'stable', since: '9.4.0' },
  },
});
		
注意:这在生成的 OAS 中是如何显示的

例如,stability: 'stable' 加上 since: '9.4.0' 在生成的文档中该字段上会变成 x-state: Generally available; added in 9.4.0

公共路由的 OAS 会作为快照写入 Kibana 仓库,最终将发布出去。

警告:建设中

在撰写本文时,我们仅捕获 Kibana HTTP API 子集的 OAS,以便让团队有时间检查并提高生成的 OAS 的质量。

如果您希望将端点的 OAS 包含在快照中,请联系 Kibana 核心团队或按照以下说明操作。

要将 OAS 发布到我们的文档网站,请创建一个 pull request,更新 此命令 以包含您的 HTTP API 路径。

OAS 将被推送到并发布到我们由 bump.sh 托管的 statefulserverless 文档中。

如果您想在合并前预览文档,可以执行以下操作

  1. 安装 bump cli: https://npmjs.net.cn/package/bump-cli
  2. 将您的文档保存到本地文件 curl localhost:5601/api/oas\?access\=public\&version\=2023-10-31\&pathStartsWith\=/api/saved_objects/_export > temp.json
  3. npx bump preview temp.json
  4. 完成后,您的文档应托管在 bump.sh 提供的临时位置

团队已为各自的 HTTP API 采用了不同的运行时验证库。Kibana 核心无意支持所有运行时验证库。

如果您在使用 @kbn/config-schema 时遇到问题、疑虑或困惑,请联系 Kibana 核心团队,我们将帮助您找到解决方案。

可以为 access: 'internal' 路由生成 OpenAPI 规范,但这不是必需的。其好处主要在于为您团队的内部参考以及其他团队发现您的 API。如果您遵循本教程中概述的实践,那么为内部路由生成 OAS 也应该很简单。

© . This website operates independently and is not affiliated with or endorsed by Elasticsearch B.V. All brand names, logos, and trademarks are the property of their respective owners.