从 OpenAPI 生成 Kubernetes API Delphi 客户端

每个 Kubernetes 集群都会描述自己的 API,sgcOpenAPI 把这份描述变成一个可以从 Delphi 调用的 Pascal 单元。这是这些页面上最大的公开规范,也是唯一一个源文档的选择会真正改变结果的规范,因此本页会说明该用哪个端点生成,以及两个已知限制是什么。

Kubernetes + sgcOpenAPI

下面的一切都来自对 Kubernetes 规范运行生成器并编译其结果的实测,不是估算。

用这个生成

/openapi/v3 下按 API 组划分的 OpenAPI 3 文档,一次一个。这条路线才能产出正确的客户端。

不要用这个

聚合的 /openapi/v2 文档。它是 Swagger 2.0,能够转换也能够生成,但有两处结果是错的。两者都在下文说明。

身份验证

使用 -a 2 生成,并设置 Authentication.Token.BearerToken,它会把 ServiceAccount 令牌作为 Authorization: Bearer 发送。

基础 URL

Kubernetes 没有声明服务器主机,因此在生成时传入 -u https://cluster:6443,或在运行时调用 SetBaseURL。

/openapi/v3 是一个索引,不是一份文档

这是首先要弄清楚的一点。集群并不提供单一的 OpenAPI 3 文件。/openapi/v3 返回一份组路径清单,每个组和版本都是位于各自 URL 上的独立文档。

# 每个 API 组和版本一份文档,标准集群中共 65 份
> sgcOpenAPI.exe -i "https://10.0.0.1:6443/openapi/v3/api/v1" \
      -o "k8s_core_v1.pas" -a 2 -u "https://10.0.0.1:6443"

File successfully created k8s_core_v1.pas

> sgcOpenAPI.exe -i "https://10.0.0.1:6443/openapi/v3/apis/apps/v1" \
      -o "k8s_apps_v1.pas" -a 2 -u "https://10.0.0.1:6443"

File successfully created k8s_apps_v1.pas

这样生成时,core 组给出 113 条路径、248 个方法和 293 个模型类,单元共 42,705 行;apps 组给出 38 条路径、77 个方法和 180 个模型类,共 17,061 行。两者都在 RAD Studio 12 的 Win32 平台上干净编译通过,库路径上除了 sgcOpenAPI 的 Source 文件夹之外无需任何内容。每组一个单元也比一个巨大的单元更好维护,而且您只需生成实际会调用的组。

同样的文档也发布在 Kubernetes 仓库的 api/openapi-spec/v3/ 目录下,因此您可以从已入库的文件生成,而不是从运行中的集群生成,并把结果纳入版本控制。

列出命名空间中的 Pod

Kubernetes 的操作 id 本身就是合法标识符,因此生成的方法保留规范中的名称。

uses
  k8s_core_v1;   // 刚刚生成的单元

procedure TfrmK8s.btnPodsClick(Sender: TObject);
var
  oResponse: TsgcOpenAPI_listCoreV1NamespacedPod_Response;
  oPod: TsgcOpenAPI_io_k8s_api_core_v1_Pod_Class;
begin
  GetOpenAPIClient.Authentication.Token.BearerToken := vToken;
  GetOpenAPIClient.SetBaseURL('https://10.0.0.1:6443');

  // aNamespace、aPretty,然后是按声明顺序排列的查询参数
  oResponse := GetOpenAPIClient.listCoreV1NamespacedPod('production',
    '', oapiBoolNull, '', '', 'app=api,tier=backend');
  try
    if oResponse.IsSuccessful then
      for oPod in oResponse.Successful.Items do
        memoLog.Lines.Add(Format('%-30s %s %s',
          [oPod.Metadata.Name, oPod.Status.Phase, oPod.Status.HostIP]))
    else
      memoLog.Lines.Add(IntToStr(oResponse.ResponseCode) + ' ' +
        oResponse.ResponseError);
  finally
    oResponse.Free;
  end;
end;

模型类沿用完整的 Kubernetes 名称,因此一个 Pod 是 TsgcOpenAPI_io_k8s_api_core_v1_Pod_Class,带有 Metadata、Spec 和 Status,列表则是 TsgcOpenAPI_io_k8s_api_core_v1_PodList_Class,带有类型化的 Items。Kubernetes 声明为可选的布尔查询参数会变成 TsgcOpenAPIBoolean,它的 oapiBoolNull 值表示该参数从 URL 中省略,而不是按 false 发送。

扩展 Deployment

凡是 Kubernetes 用具名模式描述请求体的地方,通常都是如此,生成的方法接受类型化的类而不是字符串。

uses
  k8s_apps_v1;

var
  oRead: TsgcOpenAPI_readAppsV1NamespacedDeploymentScale_Response;
  oWrite: TsgcOpenAPI_replaceAppsV1NamespacedDeploymentScale_Response;
begin
  oRead := GetOpenAPIClient.readAppsV1NamespacedDeploymentScale(
    'api', 'production');
  try
    if not oRead.IsSuccessful then
      raise Exception.Create(oRead.ResponseError);

    oRead.Successful.Spec.Replicas := 5;

    oWrite := GetOpenAPIClient.replaceAppsV1NamespacedDeploymentScale(
      'api', 'production', oRead.Successful);
    try
      memoLog.Lines.Add(IntToStr(oWrite.ResponseCode));
    finally
      oWrite.Free;
    end;
  finally
    oRead.Free;
  end;
end;

在整个 Kubernetes 接口范围内,两者比例接近对半,大约一半的请求体是类型化的类,例如 TsgcOpenAPI_io_k8s_api_autoscaling_v1_Scale_Class,另一半是字符串,取决于文档是否为模式命名。

生成的单元包含什么

单元是您所指向的那份文档的镜像,因此覆盖范围恰好就是您生成的那个组和版本。

工作负载

apis/apps/v1 提供 Deployment、StatefulSet、DaemonSet 和 ReplicaSet。apis/batch/v1 提供 Job 和 CronJob。api/v1 提供 Pod 和 ReplicationController。

服务与网络

api/v1 提供 Service、Endpoints、ConfigMap 和 Secret。apis/networking.k8s.io/v1 提供 Ingress 和 NetworkPolicy。apis/discovery.k8s.io/v1 提供 EndpointSlice。

存储、RBAC 与策略

apis/storage.k8s.io/v1、apis/rbac.authorization.k8s.io/v1、apis/policy/v1 和 apis/admissionregistration.k8s.io/v1,每一个都是独立的文档,也是独立的单元。

自定义资源

安装了 CRD 的集群会在各自的组路径上提供它们,因此 /openapi/v3 的索引会把它们与内置组一同列出,生成方式也完全相同。

每个模型一个类

ObjectMeta、PodSpec、PodStatus、Container、各种 condition 类型和 list 类型。仅 core 组就产出 293 个。

来自规范的文档

Kubernetes 的字段描述会作为 Pascal 注释出现在每个方法和属性上方,这也是单元篇幅很长的主要原因之一。

我们实际运行后发现了什么

其中两条只针对聚合的 Swagger 2.0 文档,本页之所以建议按组生成,原因正在于此。

v2 文档会丢失路径级参数

Kubernetes 在 path item 上只声明一次 namespace 和 pretty,再用 $ref 引用它们。从 /openapi/v2 生成时,这两个参数根本到不了方法,因此 listCoreV1NamespacedPod 没有 aNamespace 参数,URL 里还留着 {namespace} 占位符。改用同一个组的 /openapi/v3 文档生成,参数就在。请使用 v3 文档。

CRD 组无法编译

apis/apiextensions.k8s.io/v1 把 JSONSchemaProps.enum 描述为一个完全没有声明类型的模式的数组,因此生成器输出 TArray<>,编译器会拒绝它。它在单元里只占两行。跳过这个组,或者在生成聚合文档时把那 14 个操作传给 -x 并同时使用 -p,这会移除该类,整个单元随后即可编译。

聚合文档非常庞大

602 条路径、1,202 个方法,单元约 197,000 行、13 MB。它在一秒内完成读取和生成,但 IDE 编辑器处理这么大的文件会变慢。按组生成的单元只是其中一小部分,core 组 42,705 行,apps 组 17,061 行。

集群 CA 由您自己决定是否信任

大多数集群使用自签名 CA,因此在您指明信任什么之前,TLS 握手会失败。生成的客户端公开了 OnSSLVerifyPeer、OnSSLGetHandler 和 OnSSLAfterCreateHandler,您可以在这些事件里安装来自 kubeconfig 的集群 CA 包。

watch 不是可枚举对象

watch=true 以及带 follow=true 的容器日志都是长期存在的分块响应。生成的方法是一次普通请求,响应结束时就返回,因此实时 watch 需要您在 TsgcHTTP1Client 之上自行实现,生成的客户端不会为您做流式处理。

绑定的 ServiceAccount 令牌会过期

自 Kubernetes 1.21 起,它们就带有时限。请从集群内重新读取 /var/run/secrets/kubernetes.io/serviceaccount/token,或从外部调用 TokenRequest API,然后再次给 Authentication.Token.BearerToken 赋值。客户端只会保留您最后一次给它的值。

来自博客

OpenAPI Delphi 解析器

读取器如何处理真实的规范,以及在无法处理某些内容时它会在 Warnings 中记录什么。

阅读文章 →

OpenAPI 客户端 + 解析器

介绍生成的客户端及其所基于的读取器的配套文章。

阅读文章 →

sgcOpenAPI 2026.6

当前版本的发行说明,包含生成器选项和读取器变更。

阅读文章 →
超值之选:All-AccesseSeGeCe 全部产品,含高级支持,每年 €1,059 起。
查看 All-Access 价格

今天就从 Delphi 驱动 Kubernetes

sgcOpenAPI 随附读取器、代码生成器、OpenAPI 服务器,以及 Amazon、Azure、Google 和 Microsoft 的预构建 SDK。一个产品,三个层级,按席位计价而不是按功能计价。